cloudflare / cloudflare/agents

Potential settlement gating issue: paid tool callback runs before settlement

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

Description

Hi, I noticed a possible settlement-ordering issue in the paid MCP tool wrapper.

In `packages/agents/src/mcp/x402.ts`, the wrapper builds payment requirements for a tool:

```ts
146: // Build v2 payment requirements for this tool call
147: const resourceConfig: ResourceConfig = {
148: scheme: "exact",
149: payTo: cfg.recipient,
150: price: priceUSD,
151: network,
152: maxTimeoutSeconds: 300
153: };
168: const resourceInfo = {
169: url: `x402://${name}`,
170: description,
171: mimeType: "application/json"
172: };
```

It decodes and verifies the submitted payment:

```ts
202: // Decode the payment payload (base64-encoded JSON)
203: let paymentPayload: PaymentPayload;
205: paymentPayload = JSON.parse(atob(token));
210: // Find matching requirements for this payment
211: const matchingReq = resourceServer.findMatchingRequirements(
212: requirements,
213: paymentPayload
214: );
219: // Verify payment with facilitator
221: const vr = await resourceServer.verifyPayment(
222: paymentPayload,
223: matchingReq
224: );
225: if (!vr.isValid) {
226: return paymentRequired(vr.invalidReason ?? "INVALID_PAYMENT", {
```

After verification, the tool callback executes before settlement:

```ts
234: // Execute the tool callback
235: let result: CallToolResult;
236: let failed = false;
237: try {
238: result = await cb(args, extra);
...
257: // Settle payment only on success
258: if (!failed) {
259: try {
260: const s = await resourceServer.settlePayment(
261: paymentPayload,
262: matchingReq
263: );
264: if (s.success) {
```

The effective order is:

```text
payment payload -> matching requirement -> verifyPayment -> cb(args, extra) -> settlePayment
```

Why this may matter:

- A paid MCP tool callback may perform expensive compute, network calls, or external side effects.
- If settlement fails after the callback returns, the wrapper can return a payment-required response, but the callback work has already happened.
- The wrapper is framework-level, so it cannot assume every callback is cheap or reversible.

A safer design would settle before invoking externally effective callbacks, or explicitly restrict pre-settlement callbacks to side-effect-free work. I am reporting this as a potential issue rather than a confirmed exploit, since impact depends on what tool callbacks are registered.

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.