Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions typescript/.changeset/silver-mails-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@x402/mcp": patch
---

Preserve existing MCP response metadata when adding x402 payment metadata.
5 changes: 4 additions & 1 deletion typescript/packages/mcp/src/server/paymentWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,10 @@ async function settlePaymentResult(

return {
...result,
_meta: { [MCP_PAYMENT_RESPONSE_META_KEY]: settleResult },
_meta: {
...(result._meta as Record<string, unknown> | undefined),
[MCP_PAYMENT_RESPONSE_META_KEY]: settleResult,
},
};
} catch (settleError) {
return createSettlementFailedResult(
Expand Down
8 changes: 6 additions & 2 deletions typescript/packages/mcp/src/utils/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,18 @@ export function extractPaymentFromMeta(
* @param params - Original request params containing name and optional arguments
* @param params.name - The tool name
* @param params.arguments - Optional tool arguments
* @param params._meta - Optional existing metadata to preserve
* @param paymentPayload - Payment payload to attach
* @returns New params object with payment in _meta
*/
export function attachPaymentToMeta(
params: { name: string; arguments?: Record<string, unknown> },
params: { name: string; arguments?: Record<string, unknown>; _meta?: Record<string, unknown> },
paymentPayload: PaymentPayload,
): MCPRequestParamsWithMeta {
return {
...params,
_meta: {
...params._meta,
[MCP_PAYMENT_META_KEY]: paymentPayload,
},
};
Expand Down Expand Up @@ -155,16 +157,18 @@ interface ResultContentItem {
* @param result - Original result object containing content and optional isError flag
* @param result.content - The tool result content array
* @param result.isError - Optional flag indicating if the result is an error
* @param result._meta - Optional existing metadata to preserve
* @param settleResponse - Settlement response to attach
* @returns New result object with payment response in _meta
*/
export function attachPaymentResponseToMeta(
result: { content: ResultContentItem[]; isError?: boolean },
result: { content: ResultContentItem[]; isError?: boolean; _meta?: Record<string, unknown> },
settleResponse: SettleResponse,
): MCPResultWithMeta {
return {
...result,
_meta: {
...result._meta,
[MCP_PAYMENT_RESPONSE_META_KEY]: settleResponse,
},
};
Expand Down
28 changes: 28 additions & 0 deletions typescript/packages/mcp/test/unit/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,34 @@ describe("createPaymentWrapper", () => {
expect(result._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse);
});

it("should preserve existing metadata from handler result", async () => {
const paid = createPaymentWrapper(
mockResourceServer as unknown as Parameters<typeof createPaymentWrapper>[0],
{
accepts: [mockPaymentRequirements],
},
);

const handlerMeta = {
traceId: "trace_123",
evidence: { ledgerId: "ledger_1" },
};
const handler = vi.fn().mockResolvedValue({
content: [{ type: "text", text: "success" }],
_meta: handlerMeta,
});

const wrappedHandler = paid(handler);
const result = await wrappedHandler(
{ test: "arg" },
{ _meta: { "x402/payment": mockPaymentPayload } },
);

expect(result._meta?.traceId).toBe("trace_123");
expect(result._meta?.evidence).toEqual({ ledgerId: "ledger_1" });
expect(result._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse);
});

it("should not settle payment if tool returns error", async () => {
const paid = createPaymentWrapper(
mockResourceServer as unknown as Parameters<typeof createPaymentWrapper>[0],
Expand Down
32 changes: 32 additions & 0 deletions typescript/packages/mcp/test/unit/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ describe("attachPaymentToMeta", () => {

expect(result._meta?.[MCP_PAYMENT_META_KEY]).toEqual(mockPaymentPayload);
});

it("should preserve existing metadata when attaching payment", () => {
const params = {
name: "test_tool",
_meta: {
traceId: "trace_123",
authHint: { subject: "agent_1" },
},
};

const result = attachPaymentToMeta(params, mockPaymentPayload);

expect(result._meta?.traceId).toBe("trace_123");
expect(result._meta?.authHint).toEqual({ subject: "agent_1" });
expect(result._meta?.[MCP_PAYMENT_META_KEY]).toEqual(mockPaymentPayload);
});
});

// ============================================================================
Expand Down Expand Up @@ -226,6 +242,22 @@ describe("attachPaymentResponseToMeta", () => {
expect(withMeta.isError).toBe(false);
expect(withMeta._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse);
});

it("should preserve existing metadata when attaching settle response", () => {
const result = {
content: [{ type: "text" as const, text: "result" }],
_meta: {
traceId: "trace_123",
evidence: { ledgerId: "ledger_1" },
},
};

const withMeta = attachPaymentResponseToMeta(result, mockSettleResponse);

expect(withMeta._meta?.traceId).toBe("trace_123");
expect(withMeta._meta?.evidence).toEqual({ ledgerId: "ledger_1" });
expect(withMeta._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse);
});
});

// ============================================================================
Expand Down
Loading