@civic/x402-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.env.example +10 -0
  3. package/.github/workflows/ci.yml +144 -0
  4. package/.github/workflows/release.yml +56 -0
  5. package/.idea/modules.xml +8 -0
  6. package/.idea/vcs.xml +6 -0
  7. package/.idea/x402-mcp-example.iml +13 -0
  8. package/CLAUDE.md +31 -0
  9. package/DEVELOPER.md +181 -0
  10. package/README.md +274 -0
  11. package/biome.json +63 -0
  12. package/dist/example/client/client.d.ts +3 -0
  13. package/dist/example/client/client.d.ts.map +1 -0
  14. package/dist/example/client/client.js +93 -0
  15. package/dist/example/client/client.js.map +1 -0
  16. package/dist/example/client.d.ts +3 -0
  17. package/dist/example/client.d.ts.map +1 -0
  18. package/dist/example/client.js +105 -0
  19. package/dist/example/client.js.map +1 -0
  20. package/dist/example/config.d.ts +15 -0
  21. package/dist/example/config.d.ts.map +1 -0
  22. package/dist/example/config.js +20 -0
  23. package/dist/example/config.js.map +1 -0
  24. package/dist/example/proxy/client-proxy.d.ts +3 -0
  25. package/dist/example/proxy/client-proxy.d.ts.map +1 -0
  26. package/dist/example/proxy/client-proxy.js +88 -0
  27. package/dist/example/proxy/client-proxy.js.map +1 -0
  28. package/dist/example/proxy/client.d.ts +3 -0
  29. package/dist/example/proxy/client.d.ts.map +1 -0
  30. package/dist/example/proxy/client.js +97 -0
  31. package/dist/example/proxy/client.js.map +1 -0
  32. package/dist/example/proxy/server-proxy.d.ts +3 -0
  33. package/dist/example/proxy/server-proxy.d.ts.map +1 -0
  34. package/dist/example/proxy/server-proxy.js +72 -0
  35. package/dist/example/proxy/server-proxy.js.map +1 -0
  36. package/dist/example/server.d.ts +2 -0
  37. package/dist/example/server.d.ts.map +1 -0
  38. package/dist/example/server.js +76 -0
  39. package/dist/example/server.js.map +1 -0
  40. package/dist/example/service.d.ts +4 -0
  41. package/dist/example/service.d.ts.map +1 -0
  42. package/dist/example/service.js +15 -0
  43. package/dist/example/service.js.map +1 -0
  44. package/dist/scripts/analyzePayment.d.ts +3 -0
  45. package/dist/scripts/analyzePayment.d.ts.map +1 -0
  46. package/dist/scripts/analyzePayment.js +25 -0
  47. package/dist/scripts/analyzePayment.js.map +1 -0
  48. package/dist/scripts/client-proxy.d.ts +3 -0
  49. package/dist/scripts/client-proxy.d.ts.map +1 -0
  50. package/dist/scripts/client-proxy.js +126 -0
  51. package/dist/scripts/client-proxy.js.map +1 -0
  52. package/dist/scripts/generateWallet.d.ts +3 -0
  53. package/dist/scripts/generateWallet.d.ts.map +1 -0
  54. package/dist/scripts/generateWallet.js +15 -0
  55. package/dist/scripts/generateWallet.js.map +1 -0
  56. package/dist/src/client.d.ts +11 -0
  57. package/dist/src/client.d.ts.map +1 -0
  58. package/dist/src/client.js +52 -0
  59. package/dist/src/client.js.map +1 -0
  60. package/dist/src/client.test.d.ts +2 -0
  61. package/dist/src/client.test.d.ts.map +1 -0
  62. package/dist/src/client.test.js +178 -0
  63. package/dist/src/client.test.js.map +1 -0
  64. package/dist/src/index.d.ts +4 -0
  65. package/dist/src/index.d.ts.map +1 -0
  66. package/dist/src/index.js +7 -0
  67. package/dist/src/index.js.map +1 -0
  68. package/dist/src/index.test.d.ts +2 -0
  69. package/dist/src/index.test.d.ts.map +1 -0
  70. package/dist/src/index.test.js +27 -0
  71. package/dist/src/index.test.js.map +1 -0
  72. package/dist/src/mcpClientWithX402.d.ts +14 -0
  73. package/dist/src/mcpClientWithX402.d.ts.map +1 -0
  74. package/dist/src/mcpClientWithX402.js +78 -0
  75. package/dist/src/mcpClientWithX402.js.map +1 -0
  76. package/dist/src/proxy/client.d.ts +23 -0
  77. package/dist/src/proxy/client.d.ts.map +1 -0
  78. package/dist/src/proxy/client.js +39 -0
  79. package/dist/src/proxy/client.js.map +1 -0
  80. package/dist/src/proxy/client.test.d.ts +2 -0
  81. package/dist/src/proxy/client.test.d.ts.map +1 -0
  82. package/dist/src/proxy/client.test.js +167 -0
  83. package/dist/src/proxy/client.test.js.map +1 -0
  84. package/dist/src/proxy/hooks/apiKeyHook.d.ts +22 -0
  85. package/dist/src/proxy/hooks/apiKeyHook.d.ts.map +1 -0
  86. package/dist/src/proxy/hooks/apiKeyHook.js +72 -0
  87. package/dist/src/proxy/hooks/apiKeyHook.js.map +1 -0
  88. package/dist/src/proxy/hooks/apiKeyHook.test.d.ts +2 -0
  89. package/dist/src/proxy/hooks/apiKeyHook.test.d.ts.map +1 -0
  90. package/dist/src/proxy/hooks/apiKeyHook.test.js +240 -0
  91. package/dist/src/proxy/hooks/apiKeyHook.test.js.map +1 -0
  92. package/dist/src/proxy/index.d.ts +4 -0
  93. package/dist/src/proxy/index.d.ts.map +1 -0
  94. package/dist/src/proxy/index.js +4 -0
  95. package/dist/src/proxy/index.js.map +1 -0
  96. package/dist/src/proxy/server.d.ts +18 -0
  97. package/dist/src/proxy/server.d.ts.map +1 -0
  98. package/dist/src/proxy/server.js +35 -0
  99. package/dist/src/proxy/server.js.map +1 -0
  100. package/dist/src/proxy/server.test.d.ts +2 -0
  101. package/dist/src/proxy/server.test.d.ts.map +1 -0
  102. package/dist/src/proxy/server.test.js +26 -0
  103. package/dist/src/proxy/server.test.js.map +1 -0
  104. package/dist/src/server.d.ts +56 -0
  105. package/dist/src/server.d.ts.map +1 -0
  106. package/dist/src/server.js +320 -0
  107. package/dist/src/server.js.map +1 -0
  108. package/dist/src/server.test.d.ts +2 -0
  109. package/dist/src/server.test.d.ts.map +1 -0
  110. package/dist/src/server.test.js +666 -0
  111. package/dist/src/server.test.js.map +1 -0
  112. package/dist/src/util.d.ts +24 -0
  113. package/dist/src/util.d.ts.map +1 -0
  114. package/dist/src/util.js +47 -0
  115. package/dist/src/util.js.map +1 -0
  116. package/dist/src/util.test.d.ts +2 -0
  117. package/dist/src/util.test.d.ts.map +1 -0
  118. package/dist/src/util.test.js +71 -0
  119. package/dist/src/util.test.js.map +1 -0
  120. package/dist/src/x402Transport.d.ts +45 -0
  121. package/dist/src/x402Transport.d.ts.map +1 -0
  122. package/dist/src/x402Transport.js +288 -0
  123. package/dist/src/x402Transport.js.map +1 -0
  124. package/example/client.ts +125 -0
  125. package/example/config.ts +25 -0
  126. package/example/proxy/README.md +95 -0
  127. package/example/proxy/client-proxy.ts +102 -0
  128. package/example/proxy/client.ts +112 -0
  129. package/example/proxy/server-proxy.ts +80 -0
  130. package/example/server.ts +104 -0
  131. package/example/service.ts +19 -0
  132. package/example-client-proxy.sh +3 -0
  133. package/mcp-servers.json +9 -0
  134. package/package.json +53 -0
  135. package/scripts/analyzePayment.ts +25 -0
  136. package/scripts/client-proxy.ts +142 -0
  137. package/scripts/generateWallet.ts +17 -0
  138. package/src/client.test.ts +237 -0
  139. package/src/client.ts +61 -0
  140. package/src/index.test.ts +34 -0
  141. package/src/index.ts +7 -0
  142. package/src/proxy/client.test.ts +199 -0
  143. package/src/proxy/client.ts +61 -0
  144. package/src/proxy/hooks/apiKeyHook.test.ts +276 -0
  145. package/src/proxy/hooks/apiKeyHook.ts +77 -0
  146. package/src/proxy/index.ts +3 -0
  147. package/src/proxy/server.test.ts +33 -0
  148. package/src/proxy/server.ts +43 -0
  149. package/src/server.test.ts +822 -0
  150. package/src/server.ts +451 -0
  151. package/src/util.test.ts +83 -0
  152. package/src/util.ts +48 -0
  153. package/tsconfig.json +20 -0
  154. package/vitest.config.ts +26 -0
package/src/server.ts ADDED
@@ -0,0 +1,451 @@
1
+ import type { OutgoingHttpHeader, OutgoingHttpHeaders } from 'node:http';
2
+ import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ import {
5
+ type CallToolRequest,
6
+ isJSONRPCError,
7
+ isJSONRPCRequest,
8
+ isJSONRPCResponse,
9
+ type JSONRPCError,
10
+ type JSONRPCMessage,
11
+ type JSONRPCRequest,
12
+ type JSONRPCResponse,
13
+ type MessageExtraInfo,
14
+ type RequestId,
15
+ } from '@modelcontextprotocol/sdk/types.js';
16
+ import type { IncomingMessage, ServerResponse } from 'http';
17
+ import { type Address, getAddress } from 'viem';
18
+ import { exact } from 'x402/schemes';
19
+ import { findMatchingPaymentRequirements, processPriceToAtomicAmount } from 'x402/shared';
20
+ import {
21
+ type FacilitatorConfig,
22
+ type PaymentPayload,
23
+ type PaymentRequirements,
24
+ type Price,
25
+ settleResponseHeader,
26
+ } from 'x402/types';
27
+ import { useFacilitator } from 'x402/verify';
28
+
29
+ interface X402TransportOptions {
30
+ payTo: Address;
31
+ facilitator?: FacilitatorConfig;
32
+ sessionIdGenerator?: () => string;
33
+ enableJsonResponse?: boolean;
34
+ toolPricing?: Record<string, string>;
35
+ }
36
+
37
+ interface SettlementInfo {
38
+ transactionHash?: string;
39
+ error?: string;
40
+ }
41
+
42
+ interface ToolCallParams {
43
+ name: string;
44
+ arguments?: CallToolRequest['params']['arguments'];
45
+ }
46
+
47
+ function isToolCallParams(params: unknown): params is ToolCallParams {
48
+ return (
49
+ params !== null &&
50
+ typeof params === 'object' &&
51
+ 'name' in params &&
52
+ typeof (params as { name: unknown }).name === 'string'
53
+ );
54
+ }
55
+
56
+ interface PaymentInfo {
57
+ payment: PaymentPayload; // TODO: Type this based on x402 payment structure
58
+ toolName?: string;
59
+ toolPrice?: Price;
60
+ request?: JSONRPCRequest;
61
+ req?: IncomingMessage;
62
+ }
63
+
64
+ export class X402StreamableHTTPServerTransport {
65
+ private transport: StreamableHTTPServerTransport;
66
+ private payTo: Address;
67
+ private facilitator?: FacilitatorConfig;
68
+ private settlementMap: Map<string | number, SettlementInfo> = new Map();
69
+ private requestPaymentMap: Map<string | number, PaymentInfo> = new Map();
70
+ private pendingPayment: PaymentInfo | null = null;
71
+ private toolPricing: Record<string, Price>;
72
+ private currentResponse: ServerResponse | null = null;
73
+ private responsePaymentHeaders: Map<ServerResponse, string> = new Map();
74
+
75
+ constructor(options: X402TransportOptions) {
76
+ this.transport = new StreamableHTTPServerTransport({
77
+ sessionIdGenerator: options.sessionIdGenerator,
78
+ enableJsonResponse: options.enableJsonResponse ?? true, // Default to JSON responses
79
+ });
80
+
81
+ this.payTo = options.payTo;
82
+ this.facilitator = options.facilitator;
83
+ this.toolPricing = options.toolPricing || {};
84
+
85
+ console.log('🔧 [X402Transport] Created with payTo:', this.payTo);
86
+ console.log(' Tool pricing:', this.toolPricing);
87
+
88
+ // Intercept messages to handle payment verification and settlement
89
+ this.setupMessageInterception();
90
+ }
91
+
92
+ // Delegate transport methods
93
+ get sessionId() {
94
+ return this.transport.sessionId;
95
+ }
96
+ get onclose() {
97
+ return this.transport.onclose;
98
+ }
99
+ set onclose(handler) {
100
+ this.transport.onclose = handler;
101
+ }
102
+ get onerror() {
103
+ return this.transport.onerror;
104
+ }
105
+ set onerror(handler) {
106
+ this.transport.onerror = handler;
107
+ }
108
+ get onmessage() {
109
+ return this.transport.onmessage;
110
+ }
111
+ set onmessage(handler) {
112
+ this.transport.onmessage = handler;
113
+ }
114
+
115
+ async start() {
116
+ return this.transport.start();
117
+ }
118
+
119
+ async close() {
120
+ return this.transport.close();
121
+ }
122
+
123
+ async send(
124
+ message: JSONRPCMessage,
125
+ options?: {
126
+ relatedRequestId?: RequestId;
127
+ }
128
+ ) {
129
+ // Intercept responses to include settlement info
130
+ if ((isJSONRPCResponse(message) || isJSONRPCError(message)) && message.id !== undefined) {
131
+ const paymentInfo = this.requestPaymentMap.get(message.id);
132
+ if (paymentInfo && !this.settlementMap.has(message.id)) {
133
+ const settlementInfo = await this.settlePayment(paymentInfo, message);
134
+ this.settlementMap.set(message.id, settlementInfo);
135
+
136
+ // Include settlement info in response
137
+ if (isJSONRPCResponse(message) && settlementInfo.transactionHash) {
138
+ message = {
139
+ ...message,
140
+ result: {
141
+ ...message.result,
142
+ x402Settlement: {
143
+ transactionHash: settlementInfo.transactionHash,
144
+ settled: true,
145
+ },
146
+ },
147
+ };
148
+ }
149
+ }
150
+ }
151
+
152
+ return this.transport.send(message, options);
153
+ }
154
+
155
+ async handleRequest(
156
+ req: IncomingMessage & { auth?: AuthInfo },
157
+ res: ServerResponse,
158
+ parsedBody?: unknown
159
+ ): Promise<void> {
160
+ console.log('\n📥 [X402Transport] Handling request');
161
+ console.log(' Method:', req.method);
162
+ console.log(' URL:', req.url);
163
+
164
+ // Store the response object for later use
165
+ this.currentResponse = res;
166
+
167
+ // Intercept writeHead to inject payment header
168
+ const originalWriteHead = res.writeHead.bind(res);
169
+
170
+ res.writeHead = ((statusCode: number, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[] | undefined) => {
171
+ // Check if we have a payment response header for this response
172
+ const paymentHeader = this.responsePaymentHeaders.get(res);
173
+ if (paymentHeader && headers && !Array.isArray(headers)) {
174
+ headers['X-PAYMENT-RESPONSE'] = paymentHeader;
175
+ console.log(' 💳 Added X-PAYMENT-RESPONSE header to response');
176
+ // Clean up after use
177
+ this.responsePaymentHeaders.delete(res);
178
+ }
179
+ return originalWriteHead.call(res, statusCode, headers);
180
+ }) as typeof res.writeHead;
181
+
182
+ // Only intercept POST requests to the MCP endpoint
183
+ if (req.method !== 'POST' || !parsedBody) {
184
+ return this.transport.handleRequest(req, res, parsedBody);
185
+ }
186
+
187
+ // Check if this is a tool call that requires payment
188
+ const messages = Array.isArray(parsedBody) ? parsedBody : [parsedBody];
189
+ const toolCall = messages.find(
190
+ (msg): msg is JSONRPCRequest & { params: ToolCallParams } =>
191
+ msg.method === 'tools/call' &&
192
+ msg.params &&
193
+ typeof msg.params === 'object' &&
194
+ 'name' in msg.params &&
195
+ typeof msg.params.name === 'string' &&
196
+ this.toolPricing[msg.params.name] !== undefined
197
+ );
198
+
199
+ if (!toolCall) {
200
+ console.log(' ✅ No paid tool calls, delegating to transport');
201
+ return this.transport.handleRequest(req, res, parsedBody);
202
+ }
203
+
204
+ const toolName = toolCall.params.name;
205
+ const toolPrice = this.toolPricing[toolName];
206
+ console.log(` 💰 Found paid tool call: ${toolName} (${toolPrice})`);
207
+
208
+ // Check for X-PAYMENT header
209
+ const paymentHeader = req.headers['x-payment'];
210
+ if (!paymentHeader || Array.isArray(paymentHeader)) {
211
+ console.log(' ❌ No X-PAYMENT header found, returning 402');
212
+ res.writeHead(402).end(
213
+ JSON.stringify({
214
+ x402Version: 1,
215
+ error: 'X-PAYMENT header is required',
216
+ accepts: this.getPaymentRequirementsForTool(toolName, toolPrice),
217
+ })
218
+ );
219
+ return;
220
+ }
221
+
222
+ // Decode and verify payment
223
+ let decodedPayment: PaymentPayload;
224
+ try {
225
+ decodedPayment = exact.evm.decodePayment(paymentHeader);
226
+ decodedPayment.x402Version = 1;
227
+ console.log(' 🔐 Decoded payment:', {
228
+ scheme: decodedPayment.scheme,
229
+ network: decodedPayment.network,
230
+ from: decodedPayment.payload?.authorization?.from,
231
+ to: decodedPayment.payload?.authorization?.to,
232
+ value: decodedPayment.payload?.authorization?.value,
233
+ });
234
+ } catch (error) {
235
+ console.log(' ❌ Failed to decode payment:', error);
236
+ res.writeHead(402).end(
237
+ JSON.stringify({
238
+ x402Version: 1,
239
+ error: 'Invalid payment header',
240
+ accepts: this.getPaymentRequirementsForTool(toolName, toolPrice),
241
+ })
242
+ );
243
+ return;
244
+ }
245
+
246
+ // Verify payment at HTTP level
247
+ console.log(' 🔐 Verifying payment at HTTP level...');
248
+ try {
249
+ const { verify } = useFacilitator(this.facilitator);
250
+ const paymentRequirements = this.getPaymentRequirementsForTool(toolName, toolPrice);
251
+
252
+ const selectedPaymentRequirements = findMatchingPaymentRequirements(paymentRequirements, decodedPayment);
253
+
254
+ if (!selectedPaymentRequirements) {
255
+ console.log(' ❌ No matching payment requirements');
256
+ res.writeHead(402).end(
257
+ JSON.stringify({
258
+ x402Version: 1,
259
+ error: 'Unable to find matching payment requirements',
260
+ accepts: paymentRequirements,
261
+ })
262
+ );
263
+ return;
264
+ }
265
+
266
+ const verifyResponse = await verify(decodedPayment, selectedPaymentRequirements);
267
+ console.log(' 📡 Verify response:', verifyResponse);
268
+
269
+ if (!verifyResponse.isValid) {
270
+ console.log(' ❌ Payment verification failed');
271
+ res.writeHead(402).end(
272
+ JSON.stringify({
273
+ x402Version: 1,
274
+ error: verifyResponse.invalidReason || 'Payment verification failed',
275
+ accepts: paymentRequirements,
276
+ })
277
+ );
278
+ return;
279
+ }
280
+
281
+ console.log(' ✅ Payment verified successfully');
282
+ } catch (error) {
283
+ const errorMessage = error instanceof Error ? error.message : String(error);
284
+ console.error(error);
285
+ console.log(' ❌ Verification error:', errorMessage);
286
+ res.writeHead(402).end(
287
+ JSON.stringify({
288
+ x402Version: 1,
289
+ error: errorMessage,
290
+ accepts: this.getPaymentRequirementsForTool(toolName, toolPrice),
291
+ })
292
+ );
293
+ return;
294
+ }
295
+
296
+ // Store payment for later use
297
+ this.pendingPayment = {
298
+ payment: decodedPayment,
299
+ toolName,
300
+ toolPrice,
301
+ req,
302
+ };
303
+
304
+ // Delegate to transport
305
+ return this.transport.handleRequest(req, res, parsedBody);
306
+ }
307
+
308
+ private setupMessageInterception() {
309
+ const originalOnMessage = this.transport.onmessage;
310
+
311
+ // Intercept incoming messages
312
+ this.transport.onmessage = async (message: JSONRPCMessage, extra?: MessageExtraInfo) => {
313
+ console.log(' 🔍 [X402Transport] Intercepting message:', message);
314
+
315
+ // Check if this is a tool call that requires payment
316
+ if (isJSONRPCRequest(message) && message.method === 'tools/call' && isToolCallParams(message.params)) {
317
+ const toolName = message.params.name;
318
+ const toolPrice = this.toolPricing[toolName];
319
+
320
+ if (toolPrice && this.pendingPayment) {
321
+ console.log(` 💰 Tool '${toolName}' has verified payment`);
322
+
323
+ // Track which request this payment is for
324
+ if (message.id !== undefined) {
325
+ this.requestPaymentMap.set(message.id, {
326
+ ...this.pendingPayment,
327
+ request: message, // Store the original request for settlement
328
+ });
329
+ }
330
+ }
331
+ }
332
+
333
+ // Call original handler
334
+ if (originalOnMessage) {
335
+ await originalOnMessage.call(this.transport, message, extra);
336
+ }
337
+ };
338
+ }
339
+
340
+ private getPaymentRequirementsForTool(toolName: string, price: Price): PaymentRequirements[] {
341
+ const network = 'base-sepolia'; // TODO: make configurable
342
+
343
+ const atomicAmountForAsset = processPriceToAtomicAmount(price, network);
344
+ if ('error' in atomicAmountForAsset) {
345
+ throw new Error(atomicAmountForAsset.error);
346
+ }
347
+
348
+ const { maxAmountRequired, asset } = atomicAmountForAsset;
349
+
350
+ return [
351
+ {
352
+ scheme: 'exact',
353
+ network,
354
+ maxAmountRequired,
355
+ resource: `mcp://tool/${toolName}`,
356
+ description: `Payment for MCP tool: ${toolName}`,
357
+ mimeType: 'application/json',
358
+ payTo: getAddress(this.payTo),
359
+ maxTimeoutSeconds: 60,
360
+ asset: getAddress(asset.address),
361
+ outputSchema: undefined,
362
+ extra: asset.eip712,
363
+ },
364
+ ];
365
+ }
366
+
367
+ private async settlePayment(
368
+ paymentInfo: PaymentInfo,
369
+ response: JSONRPCResponse | JSONRPCError
370
+ ): Promise<SettlementInfo> {
371
+ console.log(' 💰 [X402Transport] Settling payment for response:', response.id);
372
+
373
+ // Only settle successful responses
374
+ if (isJSONRPCError(response)) {
375
+ console.log(' ❌ Skipping settlement for error response');
376
+ return { error: 'Response is an error' };
377
+ }
378
+
379
+ try {
380
+ const { settle } = useFacilitator(this.facilitator);
381
+
382
+ // Get the original request to determine tool and pricing
383
+ const originalRequest = paymentInfo.request;
384
+
385
+ // Type guard to check if this is a tool call with proper params
386
+ if (!isToolCallParams(originalRequest?.params)) {
387
+ throw new Error('Invalid request: missing tool name');
388
+ }
389
+
390
+ const toolName = originalRequest.params.name;
391
+
392
+ if (!this.toolPricing[toolName]) {
393
+ throw new Error(`No pricing found for tool: ${toolName}`);
394
+ }
395
+
396
+ const toolPrice = this.toolPricing[toolName];
397
+ const paymentRequirements = this.getPaymentRequirementsForTool(toolName, toolPrice);
398
+
399
+ const selectedPaymentRequirements = findMatchingPaymentRequirements(paymentRequirements, paymentInfo.payment);
400
+
401
+ if (!selectedPaymentRequirements) {
402
+ throw new Error('Unable to find matching payment requirements');
403
+ }
404
+
405
+ const settleResponse = await settle(paymentInfo.payment, selectedPaymentRequirements);
406
+ console.log(' 💳 Settle response:', settleResponse);
407
+
408
+ // Store the payment response header for this response
409
+ if (this.currentResponse) {
410
+ const responseHeader = settleResponseHeader(settleResponse);
411
+ this.responsePaymentHeaders.set(this.currentResponse, responseHeader);
412
+ console.log(' 📨 Payment response header prepared:', responseHeader);
413
+ }
414
+
415
+ return {
416
+ transactionHash: settleResponse.transaction,
417
+ };
418
+ } catch (error) {
419
+ const errorMessage = error instanceof Error ? error.message : String(error);
420
+ console.log(' ❌ Settlement error:', errorMessage);
421
+ return {
422
+ error: errorMessage,
423
+ };
424
+ }
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Creates a payment-aware MCP server transport that requires X402 payments for specified tools
430
+ * @param payTo - The wallet address to receive payments
431
+ * @param toolPricing - Mapping of tool names to prices (e.g., { "my-tool": "$0.01" })
432
+ * @param options - Optional configuration
433
+ * @returns X402StreamableHTTPServerTransport configured with payment requirements
434
+ */
435
+ export function makePaymentAwareServerTransport(
436
+ payTo: Address | string,
437
+ toolPricing: Record<string, string>,
438
+ options?: {
439
+ facilitator?: FacilitatorConfig;
440
+ sessionIdGenerator?: () => string;
441
+ enableJsonResponse?: boolean;
442
+ }
443
+ ): X402StreamableHTTPServerTransport {
444
+ return new X402StreamableHTTPServerTransport({
445
+ payTo: getAddress(payTo),
446
+ toolPricing,
447
+ facilitator: options?.facilitator,
448
+ sessionIdGenerator: options?.sessionIdGenerator,
449
+ enableJsonResponse: options?.enableJsonResponse,
450
+ });
451
+ }
@@ -0,0 +1,83 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { convertHeaders } from './util.js';
3
+
4
+ describe('convertHeaders', () => {
5
+ it('should return empty object when headers are undefined', () => {
6
+ const result = convertHeaders(undefined);
7
+ expect(result).toEqual({});
8
+ });
9
+
10
+ it('should return empty object when headers are null', () => {
11
+ const result = convertHeaders(null as any);
12
+ expect(result).toEqual({});
13
+ });
14
+
15
+ it('should convert Headers instance to plain object', () => {
16
+ const headers = new Headers();
17
+ headers.append('Content-Type', 'application/json');
18
+ headers.append('Accept', 'application/json, text/event-stream');
19
+
20
+ const result = convertHeaders(headers);
21
+ expect(result).toEqual({
22
+ 'content-type': 'application/json',
23
+ accept: 'application/json, text/event-stream',
24
+ });
25
+ });
26
+
27
+ it('should convert array of tuples to plain object', () => {
28
+ const headers: [string, string][] = [
29
+ ['Content-Type', 'application/json'],
30
+ ['Accept', 'application/json, text/event-stream'],
31
+ ['X-Custom-Header', 'value'],
32
+ ];
33
+
34
+ const result = convertHeaders(headers);
35
+ expect(result).toEqual({
36
+ 'Content-Type': 'application/json',
37
+ Accept: 'application/json, text/event-stream',
38
+ 'X-Custom-Header': 'value',
39
+ });
40
+ });
41
+
42
+ it('should return plain object as-is', () => {
43
+ const headers = {
44
+ 'Content-Type': 'application/json',
45
+ Accept: 'application/json, text/event-stream',
46
+ 'X-Custom-Header': 'value',
47
+ };
48
+
49
+ const result = convertHeaders(headers);
50
+ expect(result).toEqual(headers);
51
+ });
52
+
53
+ it('should handle empty Headers instance', () => {
54
+ const headers = new Headers();
55
+ const result = convertHeaders(headers);
56
+ expect(result).toEqual({});
57
+ });
58
+
59
+ it('should handle empty array of tuples', () => {
60
+ const headers: [string, string][] = [];
61
+ const result = convertHeaders(headers);
62
+ expect(result).toEqual({});
63
+ });
64
+
65
+ it('should handle empty plain object', () => {
66
+ const headers = {};
67
+ const result = convertHeaders(headers);
68
+ expect(result).toEqual({});
69
+ });
70
+
71
+ it('should handle Headers with multiple values for same key', () => {
72
+ const headers = new Headers();
73
+ headers.append('Set-Cookie', 'session=abc');
74
+ headers.append('Set-Cookie', 'user=123');
75
+
76
+ const result = convertHeaders(headers);
77
+ // Note: In Node.js environment, Headers may handle multiple values differently
78
+ // The last value might override previous ones instead of concatenating
79
+ expect(result['set-cookie']).toBeDefined();
80
+ // Accept either the last value or concatenated values
81
+ expect(['user=123', 'session=abc, user=123']).toContain(result['set-cookie']);
82
+ });
83
+ });
package/src/util.ts ADDED
@@ -0,0 +1,48 @@
1
+ /// <reference lib="dom" />
2
+
3
+ /**
4
+ * Converts various header formats to a plain object.
5
+ *
6
+ * This is a workaround for a bug in x402-fetch where Headers objects are not
7
+ * properly preserved during 402 payment retries. The library uses the spread
8
+ * operator on headers (...init.headers), which doesn't work correctly with
9
+ * Headers objects - it spreads the object's methods instead of the actual
10
+ * header key-value pairs.
11
+ *
12
+ * Without this conversion, critical headers like 'Accept: application/json, text/event-stream'
13
+ * (required by MCP) are lost during the payment retry, causing 406 Not Acceptable errors.
14
+ *
15
+ * Fix submitted: https://github.com/coinbase/x402/pull/314
16
+ *
17
+ * This function handles three possible input formats:
18
+ * - Headers instance (from the Fetch API)
19
+ * - Array of tuples ([key, value][])
20
+ * - Plain object (Record<string, string>)
21
+ *
22
+ * @param headers - The headers in any of the supported formats
23
+ * @returns A plain object with header key-value pairs that can be safely spread
24
+ */
25
+ export function convertHeaders(headers?: HeadersInit): Record<string, string> {
26
+ const headersObject: Record<string, string> = {};
27
+
28
+ if (!headers) {
29
+ return headersObject;
30
+ }
31
+
32
+ if (headers instanceof Headers) {
33
+ // Headers object from Fetch API
34
+ headers.forEach((value, key) => {
35
+ headersObject[key] = value;
36
+ });
37
+ } else if (Array.isArray(headers)) {
38
+ // Array of tuples format: [["Content-Type", "application/json"], ...]
39
+ headers.forEach(([key, value]) => {
40
+ headersObject[key] = value;
41
+ });
42
+ } else {
43
+ // Plain object format: { "Content-Type": "application/json", ... }
44
+ Object.assign(headersObject, headers);
45
+ }
46
+
47
+ return headersObject;
48
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*", "example/**/*", "scripts/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ reporter: ['text', 'json', 'html'],
9
+ exclude: [
10
+ 'node_modules/**',
11
+ 'dist/**',
12
+ 'example/**',
13
+ 'scripts/**',
14
+ '**/*.config.ts',
15
+ '**/*.test.ts',
16
+ '**/*.spec.ts',
17
+ ],
18
+ thresholds: {
19
+ lines: 90,
20
+ functions: 85,
21
+ branches: 90,
22
+ statements: 90,
23
+ },
24
+ },
25
+ },
26
+ });