diff --git a/typescript/.changeset/silver-mails-prove.md b/typescript/.changeset/silver-mails-prove.md new file mode 100644 index 0000000000..307c953969 --- /dev/null +++ b/typescript/.changeset/silver-mails-prove.md @@ -0,0 +1,5 @@ +--- +"@x402/mcp": patch +--- + +Preserve existing MCP response metadata when adding x402 payment metadata. diff --git a/typescript/packages/mcp/src/server/paymentWrapper.ts b/typescript/packages/mcp/src/server/paymentWrapper.ts index 14ff1a4d93..de05c179fb 100644 --- a/typescript/packages/mcp/src/server/paymentWrapper.ts +++ b/typescript/packages/mcp/src/server/paymentWrapper.ts @@ -418,7 +418,10 @@ async function settlePaymentResult( return { ...result, - _meta: { [MCP_PAYMENT_RESPONSE_META_KEY]: settleResult }, + _meta: { + ...(result._meta as Record | undefined), + [MCP_PAYMENT_RESPONSE_META_KEY]: settleResult, + }, }; } catch (settleError) { return createSettlementFailedResult( diff --git a/typescript/packages/mcp/src/utils/encoding.ts b/typescript/packages/mcp/src/utils/encoding.ts index b8d694f62a..029484d0b0 100644 --- a/typescript/packages/mcp/src/utils/encoding.ts +++ b/typescript/packages/mcp/src/utils/encoding.ts @@ -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 }, + params: { name: string; arguments?: Record; _meta?: Record }, paymentPayload: PaymentPayload, ): MCPRequestParamsWithMeta { return { ...params, _meta: { + ...params._meta, [MCP_PAYMENT_META_KEY]: paymentPayload, }, }; @@ -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 }, settleResponse: SettleResponse, ): MCPResultWithMeta { return { ...result, _meta: { + ...result._meta, [MCP_PAYMENT_RESPONSE_META_KEY]: settleResponse, }, }; diff --git a/typescript/packages/mcp/test/unit/server.test.ts b/typescript/packages/mcp/test/unit/server.test.ts index 25ee708038..c9f664befd 100644 --- a/typescript/packages/mcp/test/unit/server.test.ts +++ b/typescript/packages/mcp/test/unit/server.test.ts @@ -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[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[0], diff --git a/typescript/packages/mcp/test/unit/utils.test.ts b/typescript/packages/mcp/test/unit/utils.test.ts index 80ba8dd561..0eced2f02c 100644 --- a/typescript/packages/mcp/test/unit/utils.test.ts +++ b/typescript/packages/mcp/test/unit/utils.test.ts @@ -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); + }); }); // ============================================================================ @@ -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); + }); }); // ============================================================================