cloudflare / cloudflare/agents

Propagate upstream HTTP 402 from the openApiMcpServer request callback as an MCP-level payment-required response

Open
#1,973 0 comments 0 reactions 1 assignee Claimed by @mattzcarey View on GitHub
enhancement
Dominant language
TypeScript
Stars
5.6k
Forks
711
Avg merge
1d 20h
Merged PRs (30d)
53

Description

### Problem

`openApiMcpServer()` keeps authentication host-side by design: the `request` callback attaches credentials, and the sandbox never sees them. This works well for bearer auth, but there is no equivalent story when the upstream OpenAPI service gates some operations with [x402](https://developers.cloudflare.com/agents/tools/payments/x402/) (HTTP 402 + `PAYMENT-REQUIRED`).

Today the host callback has two options, and both fall short of the caller-pays model:

1. **Host auto-pay** (`@x402/fetch` inside the callback) — works today, but the **Worker's wallet** pays. That fits an operator-resells-API model, not the case where the MCP **caller** should pay for the operations it triggers.
2. **Surface the 402 to the sandbox** as a structured return value — the model can see the payment requirements, but there is nothing it can do with them: the sandbox has no wallet (correctly so), and there is no mechanism to hand the challenge to the MCP client.

Meanwhile, the Agents SDK already has a clean caller-pays primitive: [`paidTool` / `withX402`](https://developers.cloudflare.com/agents/tools/payments/x402/charge-for-mcp-tools/) returns an MCP-level 402, and `withX402Client` signs against the advertised requirements and replays the tool call with the proof in MCP metadata. But `execute` is a single generic tool with per-operation upstream pricing, so `paidTool`'s static per-tool price doesn't fit — and today **an upstream HTTP 402 inside `execute` never becomes an MCP-level 402 that an x402-capable client could settle.**

To be precise about the gap, caller-pays through `execute` needs three pieces, and only the first is a simple mapping:

1. Upstream `PAYMENT-REQUIRED` requirements propagated up as an MCP-level 402 — a typed error/result from the `request` callback.
2. On the client's paid retry, the proof must be attached to **the one upstream request that triggered the 402** — the host needs a correlation between the MCP-level payment and a specific `request()` invocation.
3. Upstream calls that already succeeded inside the same script must **not** be re-executed.

Without (2) and (3), propagation alone produces either a full script replay (side effects re-applied) or a client that paid while the host doesn't know where to attach the signature.

### Proposed Solution

A design that keeps the runtime untouched — no script suspension, no replay:

- When the host `request` callback throws a typed `PaymentRequiredError` (carrying the parsed upstream requirements), `openApiMcpServer()` **aborts the script at that point** and returns an MCP-level 402 in the `withX402` shape, extended with a **payment intent**: an ID bound host-side to the exact pending upstream request (method, path, query, body).
- After the client pays, it retries referencing the intent ID (proof in MCP `_meta`, as `withX402Client` already does). The host executes **only that single upstream request** with `PAYMENT-SIGNATURE` attached and returns its result as the tool result.
- The model, having received the partial script output plus the settled operation's result, continues with a follow-up `execute` if needed.

This is deliberately narrower than "resume the script where it paused": `execute`'s boundary is the whole script and the `request` callback is a sandbox-to-host RPC, so suspending mid-script would be a significant runtime change. Scoping settlement to a single pending request keeps this at the level of an error mapper plus an intent store, at the cost of the model explicitly continuing after a paid operation — which seems acceptable for payment-gated writes.

Trade-off worth flagging: script work after the aborted call is lost, and scripts mixing several paid calls settle one at a time. If the team prefers a suspension-based design instead, that is a bigger ask and I'd defer to you on feasibility.

### Alternatives Considered

- **Host auto-pay, documented as the official pattern** — if caller-pays through `execute` is out of scope, a docs recipe showing `@x402/fetch` inside the `request` callback (mirroring the bearer-auth section of the [search/execute guide](https://developers.cloudflare.com/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server/)) would at least make the operator-pays model a supported, copy-pasteable path.
- **Hybrid composition recipe** — document running `openApiMcpServer()` for free reads alongside `paidTool` for the paid writes on the same server, so publishers don't have to discover this composition themselves.
- **Coarse-grained pricing** — wrapping `execute` itself with `withX402` (one flat price per script run). Possible today but a poor fit for per-operation upstream pricing.

### Additional Context

- x402 in the Agents SDK: [overview](https://developers.cloudflare.com/agents/tools/payments/x402/), [Charge for MCP tools](https://developers.cloudflare.com/agents/tools/payments/x402/charge-for-mcp-tools/)
- Code Mode search/execute guide (host-side auth pattern): [Build a search and execute MCP server](https://developers.cloudflare.com/agents/model-context-protocol/guides/build-codemode-openapi-mcp-server/)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.