Skip to content

fix(x402): read paidTool payments from MCP SDK v2 handler context - #2494

Merged
ben-reitz merged 1 commit into
mainfrom
fix/x402-mcp-sdk-v2-payment
Oct 6, 2026
Merged

ben-reitz merged 1 commit into
mainfrom
fix/x402-mcp-sdk-v2-payment

Conversation

@ben-reitz

@ben-reitz ben-reitz commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2415

Summary

paidTool read the payment from SDK v1 fields only, so on an SDK v2 server it never found one and every call returned PAYMENT_REQUIRED. The header lookup also used exact-case names. Every SDK transport lowercases header names, so header payments never matched on v1 either.

 paidTool handler(args, extra)
-  token = extra._meta["x402/payment"]
-       ?? extra.requestInfo.headers["PAYMENT-SIGNATURE"]   # exact case, never matched
-       ?? extra.requestInfo.headers["X-PAYMENT"]
+  token = readPaymentToken(extra)
+    meta    = extra.mcpReq?._meta        # v2
+           ?? extra._meta                # v1
+    headers = extra.http?.req?.headers   # v2: web Headers
+           ?? extra.requestInfo?.headers # v1: plain object, lowercased keys
+    meta["x402/payment"] ?? header("PAYMENT-SIGNATURE") ?? header("X-PAYMENT")   # case-insensitive

The order is unchanged: _meta, then PAYMENT-SIGNATURE, then X-PAYMENT.

Types: withX402 gets two overloads. The v1 overload is last, so v1 callers keep exactly the types they have on main, including generic wrappers and ReturnType<typeof withX402>.

withX402<T extends V2McpServer>(server: T, cfg): T & X402AugmentedServer<V2McpServer>  // callback gets v2 ServerContext
withX402<T extends McpServer>(server: T, cfg):   T & X402AugmentedServer               // unchanged v1 types

The v2 types are type-only imports from @modelcontextprotocol/server, which is already a required peer dependency, so there's no runtime import.

Evidence

src/x402-tests/sdk-context.test.ts uses real v1 and v2 McpServers and clients over in-memory and HTTP transports; only the facilitator is mocked. The payment cases check that the exact token the client sent reaches verifyPayment and settlePayment.

v2 in-memory   _meta payment                    → verified + settled
v2 in-memory   malformed _meta payment          → INVALID_PAYMENT
v2 HTTP        PAYMENT-SIGNATURE / Payment-Signature / payment-signature / X-PAYMENT / X-Payment → verified
v1 HTTP        PAYMENT-SIGNATURE / Payment-Signature / X-PAYMENT / x-payment → verified
v1, v2 HTTP    _meta + a bad header             → _meta wins
v1 in-memory   _meta payment                    → verified (already worked)
all            no payment                       → PAYMENT_REQUIRED
  • Before (main): 11 failed | 4 passed. Every v2 payment case and every v1 header case failed with expected 'PAYMENT_REQUIRED' to be undefined.
    After: 17/17 pass, and x402 overall 65/65.
  • Mutation checks: making header matching case-sensitive, ignoring mcpReq._meta, ignoring v2 HTTP headers, dropping X-PAYMENT, or moving headers ahead of _meta each fail tests.
  • src/tests-d/x402.test-d.ts pins the types: v2 callback context, a direct v1 call, a generic v1 wrapper, and ReturnType<typeof withX402>. Against a single conditional signature, the last two fail with TS2345/TS7031; with the overloads they compile.
  • src/tests/mcp 620/620; pnpm run check passes.

Merge Danger

Door: two-way

Blast Radius: x402 servers

Only paidTool's token lookup changes at runtime. Requests that carried a payment but got PAYMENT_REQUIRED now go on to verify and settle it, which is the intended behaviour. v1 types are unchanged.

Follow-ups, not in this PR:

  • Browser CORS: the default MCP handlers' Access-Control-Allow-Headers lists don't include PAYMENT-SIGNATURE or X-PAYMENT. Browser clients sending payment headers need corsOptions.headers set. Our own withX402Client uses _meta, so it isn't affected.
  • v2 inputRequired(...): the callback type rejects it, but if a tool bypasses that, payment settles on the first round. Multi-round payment needs its own design.

@changeset-bot

changeset-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 010da63

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment thread packages/agents/src/mcp/client/x402.ts
Comment thread packages/agents/src/mcp/client/x402.ts
@agent-think

agent-think Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

✅ agents import sizes: no significant changes (39ce1c41 → 010da634, workflow run)

paidTool read the payment token only from the SDK v1 handler context
(extra._meta and extra.requestInfo.headers). SDK v2 passes
{ sessionId, mcpReq, http }, with _meta at ctx.mcpReq._meta and the
request at ctx.http.req, so every paid call on a v2 McpServer returned
PAYMENT_REQUIRED.

The header fallback was also broken on v1: it looked up
"PAYMENT-SIGNATURE" / "X-PAYMENT" with exact case, but every SDK
transport lowercases header names, so only _meta payments worked.

Normalize both context shapes in a small readPaymentToken helper and
match header names case-insensitively. withX402 now also accepts a v2
McpServer in its types; on a v2 server the paidTool callback receives
the v2 ServerContext. withX402 is overloaded with the v1 overload last,
so generic v1 callers and ReturnType<typeof withX402> keep the v1 types.

Fixes #2415
@ben-reitz
ben-reitz force-pushed the fix/x402-mcp-sdk-v2-payment branch from 0d4c5d9 to 010da63 Compare October 5, 2026 21:15
@pkg-pr-new

pkg-pr-new Bot commented Oct 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2494

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2494

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2494

hono-agents

npm i https://pkg.pr.new/hono-agents@2494

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2494

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2494

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2494

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2494

commit: 010da63

@ben-reitz
ben-reitz merged commit 04922ec into main Oct 6, 2026
23 of 24 checks passed
@ben-reitz
ben-reitz deleted the fix/x402-mcp-sdk-v2-payment branch October 6, 2026 08:35
@github-actions github-actions Bot mentioned this pull request Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agents/x402: withX402 never sees the payment with MCP SDK v2 (reads extra._meta, v2 moved it to ctx.mcpReq._meta)

1 participant