Skip to main content

Hosted MCP OAuth event writes

Status

Locked decision: the implementation is one default-off feature behind VRDEX_HOSTED_MCP_EVENT_WRITES=false. Production activation and the first authorized Faceless create/update/readback proof remain a separate operational rollout. Anonymous hosted reads and the credentialed local stdio bridge remain available regardless of this flag.

Authorization contract

VRDex follows the MCP authorization specification for Streamable HTTP:

  • /.well-known/oauth-protected-resource/mcp identifies the exact MCP resource and its authorization server.
  • OAuth authorization-code grants use PKCE S256 and bind authorization and token requests to the MCP resource.
  • Client ID Metadata Documents are preferred when a client supports them. Constrained Dynamic Client Registration remains the compatibility fallback.
  • Access tokens include issuer, expiry, resource audience, client ID, token ID, and scopes. Every request also checks the durable token/application record so revocation takes effect before dispatch.
  • Public clients use rotating refresh tokens. Revocation is available at /oauth/revoke.
  • Native loopback callbacks honor RFC 8252 ephemeral-port behavior. For interoperability, VRDex treats localhost, IPv4 loopback, and IPv6 loopback as the same loopback target only when the HTTP scheme, path, and query match; PKCE remains mandatory.
  • Bearer tokens are accepted only in the Authorization header. They are never forwarded to Convex or /api/v0, returned in tool output, or stored in audit records.

When the flag is off, write tools are not registered, protected-resource/CIMD metadata omits their scopes, DCR rejects them, and authorization rejects an attempt to request them. When it is on, vrdex_event_create and vrdex_event_update advertise OAuth-only security metadata. Calling either tool requires a user-delegated token for the MCP resource with both mcp:write and events:write. A missing token receives 401 plus an authoritative scope challenge; an invalid token receives 401; insufficient scope or a client-credentials subject receives 403. Anonymous public reads remain available, and authenticated read calls require mcp:read. Constrained DCR accepts the exact write-only scope pair advertised by those tools; requesting only one write scope is rejected.

The canonical /mcp URL therefore initializes anonymously. Native clients whose explicit login command only starts OAuth after an initial 401 may use /mcp?auth=required. That opt-in bootstrap URL requires mcp:read at connection time while keeping the token audience and protected resource bound to canonical /mcp; it does not create a second MCP resource or change anonymous behavior at the canonical URL. Protected write calls still require both write scopes per call.

The SDK receives AuthInfo only after VRDex verifies the token. The raw token is present in process memory solely because the SDK contract requires it. Callbacks use only sanitized extra fields: durable user ID, OAuth client ID, token ID, and request ID. Each callback repeats the user/scopes check as defense in depth.

Ownership and write semantics

The MCP server does not use a shared API credential. It calls dedicated Convex mutations with the authenticated VRDex user ID. Those mutations share the same event normalization and ownership helpers as /api/v0 and require the user to own the durable published community record. Registration metadata, consent copy, or a client-supplied community slug never grants ownership.

Create and update preserve the public API contract introduced with PR #190:

  • omitted update properties preserve stored values;
  • explicit null clears supported optional scalar values;
  • empty arrays clear supported collections;
  • deterministic validation, ownership, and idempotency conflicts return a sanitized rejection and may be corrected without implying a prior commit;
  • a successful mutation is read back through the public event query;
  • an accepted write whose public readback fails returns a warning and says not to retry automatically;
  • a transport/commit outcome that cannot be proven returns an indeterminate result and says not to retry automatically.

Each call requires an operator-chosen idempotencyKey. VRDex stores only its SHA-256 hash and a canonical request fingerprint. The receipt key is scoped by user, OAuth client, and tool. An exact replay returns the original accepted result without another event or audit; reusing the key for different input fails. Tool annotations deliberately keep idempotentHint: false: receipts make an intentional same-key recovery safe but do not authorize an agent to retry automatically.

Controls and observability

  • Write traffic uses authenticated_mcp_write, limited to 30 requests per minute before the normal token/client/user aggregate limits and trusted-partner policy. A JSON-RPC batch may contain at most one hosted event write, so one accepted request cannot bypass the per-write throttle.
  • apiWriteAuditEvents records the accepted mutation, owner, client ID, token ID, request ID, tool, idempotency hash, and target IDs.
  • mcpToolEvents records accepted, denied, indeterminate, or readback-warning outcomes without request bodies, raw keys, tokens, event content, or network identities.
  • Existing OAuth validation events cover invalid, expired, revoked, wrong-resource, and under-scoped tokens.
  • Authorization-code exchange failures log only a bounded rejection category; client IDs, codes, verifiers, redirects, resources, and scopes are omitted.
  • Rollback is one setting: restore VRDEX_HOSTED_MCP_EVENT_WRITES=false and redeploy. Reads and the local bridge are unaffected. Existing tokens remain revocable but cannot reach a registered write tool.

Threat model

ThreatRequired control
Stolen or replayed bearerShort access-token lifetime, resource audience, durable revocation, header-only transport, token/client/user rate buckets
Confused deputy or cross-community writeUser-delegated subjects only and a durable owner lookup inside the transaction
Scope downgrade or client overreachExact mcp:write events:write per-call check; DCR/CIMD write scopes unavailable while default-off
Duplicate mutation after timeoutTransactional user/client/tool/key receipt plus request fingerprint; no automatic retry
Shared-secret blast radiusNo master MCP credential and no bearer forwarding
Secret/content disclosureSanitized errors and attribution-only logs; no token, raw idempotency key, or event body persistence
Metadata or redirect abuseExact HTTPS redirect matching; path/query-bound native loopback matching with PKCE; CIMD size/deadline/address restrictions; constrained DCR

Verification and client compatibility

Automated coverage must prove:

  • default-off tool and metadata omission;
  • exact 401/403 scope challenges, wrong-resource/expired/revoked rejection, and client-credentials rejection;
  • conditional DCR and CIMD scope policy, including the advertised write-only scope pair;
  • per-request AuthInfo plumbing with no raw token/key reaching Convex;
  • durable ownership rejection;
  • create/update omission and null behavior;
  • exact replay, fingerprint conflict, and client namespace isolation;
  • accepted public readback, readback warning, and indeterminate/no-retry text;
  • write rate policy, multi-write batch rejection, audit attribution, content-safe event records, and rollback to the anonymous-read-only surface.

Before production activation, run the same staged authorization-code flow and a non-Faceless disposable event against current Codex, Claude, and OpenClaw clients. Record protected-resource discovery, CIMD or DCR choice, PKCE login, refresh/relogin behavior, tool listing, user approval, one create, same-key replay, one update, and public readback. A client that does not perform lazy 401/403 step-up may use its explicit MCP login command; it is a blocker only if neither native discovery nor explicit login can establish the scoped session. No matrix row may be marked pass from protocol simulation alone.

For explicit-login clients, configure the staged server URL as https://staging.vrdex.net/mcp?auth=required, request mcp:read mcp:write events:write, and confirm the resulting token is still issued for https://staging.vrdex.net/mcp.

The Staging Deploy workflow keeps the feature off unless a manual dispatch explicitly selects hosted_mcp_event_writes. Use that staging-only switch for the client matrix, then dispatch the current main revision with the switch cleared to restore the default-off staging state. The switch is not read by production deployment workflows and does not authorize any tool call.

ClientCurrent preflightStaged scoped-session evidence
Codex CLI 0.145.0Exact-branch local Streamable HTTP initialization listed vrdex_event_create and vrdex_event_update without invoking either tool. Supports codex mcp login/logout; use --scopes mcp:read,mcp:write,events:write because an unscoped login copies the issuer-wide scope catalog.Pass at c58f546e9 in Staging Deploy run 30174911138. Native DCR, S256 PKCE consent, canonical /mcp token exchange, and persisted Auth: OAuth status succeeded. Native startup then completed authenticated initialize/notification/tool-list protocol POSTs (200/202/200) without a tool call. The isolated Codex home lacked a separate OpenAI model credential after MCP bootstrap; that does not affect the OAuth or tool-list result.
Claude Code 2.1.218Exact-branch local HTTP initialization listed vrdex_event_create and vrdex_event_update without invoking either tool. Supports claude mcp login/logout.Pass at c58f546e9. Native CIMD discovery used https://claude.ai/oauth/claude-code-client-metadata; S256 PKCE consent and token exchange succeeded, and claude mcp get/list reported the staged server connected. Final exact-head health was repeated without invoking a tool.
OpenClaw 2026.7.1-2Exact-branch isolated-state capability probe listed all ten tools, including vrdex_event_create and vrdex_event_update, without invoking either tool. Supports OAuth scope/client-metadata configuration and login/logout.Pass at c58f546e9. Native DCR, S256 PKCE consent, loopback callback/token exchange, refresh persistence, and exact-head capability probe succeeded; the probe listed all ten tools without invoking any tool.

Primary standards: