UNPKG

mcpay

Version:

SDK and CLI for MCPay functionality - MCP servers with payment capabilities

351 lines 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PaymentTransport = void 0; exports.createPaymentTransport = createPaymentTransport; const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js"); const zod_1 = require("zod"); const x402_1 = require("../x402"); /** * Custom schema for payment requirements that matches our server's format. * This is more lenient than the x402 default schema. */ const CustomPaymentRequirementsSchema = zod_1.z.object({ scheme: zod_1.z.enum(["exact"]), network: zod_1.z.enum(["base-sepolia", "base", "sei-testnet"]), // Restrict to supported networks maxAmountRequired: zod_1.z.string(), resource: zod_1.z.string(), // Allow any string, not just URLs description: zod_1.z.string().optional().default(""), mimeType: zod_1.z.string().optional().default(""), maxTimeoutSeconds: zod_1.z.number().optional().transform(val => val ?? 60), // Default to 60 if not provided asset: zod_1.z.string(), payTo: zod_1.z.string().optional(), // Make payTo optional extra: zod_1.z.record(zod_1.z.unknown()).optional() }); /** * PaymentTransport extends StreamableHTTPClientTransport to handle 402 Payment Required responses * by automatically creating and sending payment headers using the x402 payment protocol. * * This implementation uses the x402-fetch library to automatically handle payments. */ class PaymentTransport extends streamableHttp_js_1.StreamableHTTPClientTransport { _walletClient; _maxPaymentValue; _paymentRequirementsSelector; _paymentUrl; _customRequestInit; _paymentFetch; constructor(url, walletClient, opts) { // Call parent constructor with normal options first super(url, { sessionId: opts.sessionId, requestInit: opts.requestInit, authProvider: opts.authProvider, reconnectionOptions: opts.reconnectionOptions }); // Create a payment-enabled fetch function that preserves all headers const paymentFetch = async (input, init) => { // Start with the original fetch const response = await fetch(input, init); // If not a 402 response, just return it if (response.status !== 402) { return response; } // Handle 402 Payment Required response console.log("[x402] Payment required response detected"); // Parse payment requirements const { x402Version, accepts } = (await response.json()); // Parse requirements with our custom schema const parsedPaymentRequirements = accepts.map(req => { // Validate and normalize with our custom schema const validated = CustomPaymentRequirementsSchema.parse(req); // Add default payTo if missing (required by createPaymentHeader) if (!validated.payTo) { validated.payTo = validated.asset; } // Ensure maxTimeoutSeconds is set if (validated.maxTimeoutSeconds === undefined) { validated.maxTimeoutSeconds = 60; } return validated; }); // Get chain ID from wallet client const chainId = walletClient.chain?.id || walletClient.client?.chain?.id || 84532; // fallback to Base Sepolia console.log("[x402] Constructor payment fetch - Wallet client chain info:", { walletChainId: walletClient.chain?.id, clientChainId: walletClient.client?.chain?.id, resolvedChainId: chainId }); // Select appropriate payment requirements const selectedPaymentRequirements = this._paymentRequirementsSelector(parsedPaymentRequirements, chainId ? x402_1.ChainIdToNetwork[chainId] : undefined, "exact"); // Check if the payment amount exceeds the maximum allowed if (BigInt(selectedPaymentRequirements.maxAmountRequired) > this._maxPaymentValue) { throw new Error(`Payment amount ${selectedPaymentRequirements.maxAmountRequired} exceeds maximum allowed ${this._maxPaymentValue}`); } console.log("[x402] Constructor - About to create payment header with:", { walletClient: walletClient, x402Version, selectedPaymentRequirements }); // Create payment header const paymentHeader = await (0, x402_1.createPaymentHeader)(walletClient, x402Version, selectedPaymentRequirements); // Create new headers object, preserving all original headers const newHeaders = new Headers(init?.headers || {}); newHeaders.set("X-PAYMENT", paymentHeader); newHeaders.set("Access-Control-Expose-Headers", "X-PAYMENT-RESPONSE"); // Create a new request init object with all original properties const newInit = { ...init, headers: newHeaders, }; // Make the request with payment header console.log("[x402] Retrying request with payment header"); return fetch(input, newInit); }; // Wrap the payment fetch with logging const loggedPaymentFetch = async (input, init) => { // Log the request with timestamp const timestamp = new Date().toISOString(); const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; console.log(`[x402-log][${timestamp}] Request URL: ${url}`); // Log HTTP method/verb const method = init?.method || 'GET'; console.log(`[x402-log][${timestamp}] HTTP Method: ${method}`); if (init) { // Log headers console.log(`[x402-log][${timestamp}] Request Headers:`, init.headers); // Log credentials mode if (init.credentials) { console.log(`[x402-log][${timestamp}] Credentials Mode: ${init.credentials}`); } // Log payload if it exists if (init.body) { try { const bodyContent = init.body instanceof ReadableStream ? '[ReadableStream]' : typeof init.body === 'string' ? init.body : JSON.stringify(init.body); console.log(`[x402-log][${timestamp}] Request Payload:`, bodyContent); } catch (e) { console.log(`[x402-log][${timestamp}] Request Payload: [Unable to stringify payload]`); } } } console.log(`[x402-log][${timestamp}] Sending request...`); // Measure request time const startTime = performance.now(); // Convert input to compatible type for paymentFetch const fetchInput = typeof input === 'string' || input instanceof URL ? input : input.url; // Call the payment fetch const response = await paymentFetch(fetchInput, init); const endTime = performance.now(); const duration = (endTime - startTime).toFixed(2); // Log the response details console.log(`[x402-log][${timestamp}] Response received in ${duration}ms`); console.log(`[x402-log][${timestamp}] Response Status: ${response.status} ${response.statusText}`); console.log(`[x402-log][${timestamp}] Response Type: ${response.type}`); // Fix headers logging - cast headers to iterable for older TS versions const headerEntries = []; response.headers.forEach((value, key) => { headerEntries.push([key, value]); }); console.log(`[x402-log][${timestamp}] Response Headers:`, Object.fromEntries(headerEntries)); return response; }; // Store for later use this._paymentFetch = loggedPaymentFetch; if (!walletClient) { throw new Error("PaymentTransport requires a walletClient"); } this._paymentUrl = url; this._walletClient = walletClient; this._maxPaymentValue = opts.maxPaymentValue ?? BigInt(0.1 * 10 ** 6); // Default to 0.10 USDC this._paymentRequirementsSelector = opts.paymentRequirementsSelector ?? x402_1.selectPaymentRequirements; this._customRequestInit = opts.requestInit; // Override the send method to use our payment-enabled fetch this._overrideSendMethod(); } /** * Override the send method to use our payment-enabled fetch */ _overrideSendMethod() { // Store the original _commonHeaders method from parent class const originalCommonHeaders = streamableHttp_js_1.StreamableHTTPClientTransport.prototype['_commonHeaders']; // Override _commonHeaders to intercept header creation streamableHttp_js_1.StreamableHTTPClientTransport.prototype['_commonHeaders'] = async function () { // Call the original method to get standard headers const headers = await originalCommonHeaders.call(this); // Check if we're in payment mode by looking for payment headers in _customRequestInit // @ts-ignore - We're accepting the missing properties for this temporary replacement if (this['_customRequestInit']?.headers) { // @ts-ignore - We're accepting the missing properties for this temporary replacement const customHeaders = this['_customRequestInit'].headers; if (customHeaders["X-PAYMENT"]) { // Add payment headers to the common headers headers.set("X-PAYMENT", customHeaders["X-PAYMENT"]); headers.set("Access-Control-Expose-Headers", "X-PAYMENT-RESPONSE"); } } return headers; }; // Store the original send method const originalSend = this.send.bind(this); // Replace the send method with our custom implementation this.send = async (message, options) => { try { // First try the original send method return await originalSend(message, options); } catch (error) { // If we get a 402 error, retry with our payment-enabled fetch const errorMessage = String(error); if (errorMessage.includes("HTTP 402")) { console.log("[x402] Detected 402 Payment Required, handling payment..."); // Handle the payment required response await this._handlePaymentRequired(message, errorMessage, options); return; } // Not a 402 error or payment failed, re-throw throw error; } }; } /** * Extract payment requirements from an error message */ _extractPaymentRequirementsFromError(errorMessage) { try { // Find the JSON part in the error message const jsonMatch = errorMessage.match(/\{.*\}/s); if (!jsonMatch) return null; const jsonData = JSON.parse(jsonMatch[0]); if (jsonData.x402Version && Array.isArray(jsonData.accepts)) { return { x402Version: jsonData.x402Version, accepts: jsonData.accepts, error: jsonData.error }; } return null; } catch (e) { console.error("[x402] Failed to extract payment requirements from error:", e); return null; } } /** * Handle a 402 Payment Required response by creating and sending a payment header */ async _handlePaymentRequired(message, errorMessage, options) { try { // First, try to extract payment requirements from the error message let paymentRequirements; // Extract requirements from the error message if possible const extractedReqs = this._extractPaymentRequirementsFromError(errorMessage); if (extractedReqs) { console.log("[x402] Extracted payment requirements from error message"); paymentRequirements = extractedReqs; } else { // If extraction failed, fetch requirements from the API endpoint console.log("[x402] Fetching payment requirements from endpoint..."); const response = await fetch(this._paymentUrl.toString(), { method: "OPTIONS", headers: { Accept: "application/json", } }); if (!response.ok) { throw new Error(`Failed to get payment requirements: ${response.statusText}`); } paymentRequirements = await response.json(); } console.log("[x402] Payment requirements:", paymentRequirements); const { x402Version, accepts } = paymentRequirements; // Parse requirements with our custom schema instead of the default one const parsedPaymentRequirements = accepts.map(req => { // Validate and normalize with our custom schema const validated = CustomPaymentRequirementsSchema.parse(req); // Add default payTo if missing (required by createPaymentHeader) if (!validated.payTo) { validated.payTo = validated.asset; } // Ensure maxTimeoutSeconds is set if (validated.maxTimeoutSeconds === undefined) { validated.maxTimeoutSeconds = 60; } // Cast to the expected PaymentRequirements type return validated; }); // Determine chain ID from wallet client const chainId = this._walletClient.chain?.id || this._walletClient.client?.chain?.id || 84532; // fallback to Base Sepolia console.log("[x402] Wallet client chain info:", { walletChainId: this._walletClient.chain?.id, clientChainId: this._walletClient.client?.chain?.id, resolvedChainId: chainId }); console.log("[x402] Using chain ID:", chainId); // Select appropriate payment requirements const selectedPaymentRequirements = this._paymentRequirementsSelector(parsedPaymentRequirements, chainId ? x402_1.ChainIdToNetwork[chainId] : undefined, "exact"); console.log("[x402] Selected payment requirements:", selectedPaymentRequirements); // Check if the payment amount exceeds the maximum allowed if (BigInt(selectedPaymentRequirements.maxAmountRequired) > this._maxPaymentValue) { throw new Error(`Payment amount ${selectedPaymentRequirements.maxAmountRequired} exceeds maximum allowed ${this._maxPaymentValue}`); } console.log("[x402] Wallet client details:", this._walletClient); console.log("[x402] Selected payment requirements for header creation:", selectedPaymentRequirements); console.log("[x402] x402Version:", x402Version); // Create payment header console.log("[x402] Creating payment header..."); const paymentHeader = await (0, x402_1.createPaymentHeader)(this._walletClient, x402Version, selectedPaymentRequirements); console.log("[x402] Payment header created"); // Create a modified version of the requestInit with payment headers const requestInit = this._customRequestInit ? { ...this._customRequestInit } : {}; if (!requestInit.headers) { requestInit.headers = {}; } // Add payment headers const headers = requestInit.headers; headers["X-PAYMENT"] = paymentHeader; headers["Access-Control-Expose-Headers"] = "X-PAYMENT-RESPONSE"; // Instead of creating a new transport, modify the current one's request headers // and retry the request console.log("[x402] Retrying request with payment header"); // Store original requestInit const originalRequestInit = this._customRequestInit; try { // Temporarily set the requestInit with payment headers this._customRequestInit = requestInit; // Retry the request with the same transport instance await super.send(message, options); console.log("[x402] Payment successful"); } finally { // Restore original requestInit this._customRequestInit = originalRequestInit; } } catch (error) { console.error("[x402] Payment error:", error); this.onerror?.(error); throw error; } } } exports.PaymentTransport = PaymentTransport; /** * Creates a PaymentTransport instance that automatically handles 402 Payment Required responses * by creating and sending payment headers using the x402 payment protocol. */ function createPaymentTransport(url, walletClient, options) { return new PaymentTransport(url, walletClient, options || {}); } //# sourceMappingURL=payment-http-transport.js.map