UNPKG

mcpay

Version:

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

266 lines 11.4 kB
import { getAddress } from "viem"; import { decodePayment as decodeX402Payment } from "x402/schemes"; import { findMatchingPaymentRequirements, processPriceToAtomicAmount } from "x402/shared"; import { SupportedEVMNetworks, SupportedSVMNetworks } from "x402/types"; import { useFacilitator } from "x402/verify"; export function withX402(server, cfg) { const { verify, settle } = useFacilitator(cfg.facilitator); const x402Version = cfg.version ?? 1; // Normalize recipients to a per-network map, supporting evm/svm shorthands const normalizeRecipients = (r) => { if (!r || typeof r !== "object") return {}; const out = {}; // Helper to detect if a network is a testnet const isTestnetNetwork = (network) => { return (network.includes("sepolia") || network.includes("fuji") || network.includes("devnet") || network.includes("testnet") || network.includes("amoy")); }; // Expand evm/svm shorthands first const maybeFamily = r; if (maybeFamily.evm && typeof maybeFamily.evm.address === "string") { const useTestnet = maybeFamily.evm.isTestnet; for (const net of SupportedEVMNetworks) { if (useTestnet === undefined || isTestnetNetwork(net) === !!useTestnet) { out[net] = maybeFamily.evm.address; } } } if (maybeFamily.svm && typeof maybeFamily.svm.address === "string") { const useTestnet = maybeFamily.svm.isTestnet; for (const net of SupportedSVMNetworks) { if (useTestnet === undefined || isTestnetNetwork(net) === !!useTestnet) { out[net] = maybeFamily.svm.address; } } } // Copy explicit per-network mappings (override expanded ones if present) const allKnown = new Set([...SupportedEVMNetworks, ...SupportedSVMNetworks]); for (const [key, value] of Object.entries(r)) { if (typeof value === "string" && allKnown.has(key)) { out[key] = value; } } return out; }; function paidTool(name, description, price, paramsSchema, annotations, cb) { // Build synchronous payment information for annotations const recipientsByNetwork = normalizeRecipients(cfg.recipient); const paymentNetworks = []; // Build basic network info synchronously const networks = Object.keys(recipientsByNetwork); for (const network of networks) { const payTo = recipientsByNetwork[network]; if (!network || !payTo) continue; const atomic = processPriceToAtomicAmount(price, network); if ("error" in atomic) continue; const { maxAmountRequired, asset } = atomic; const networkInfo = { network, recipient: payTo, maxAmountRequired: maxAmountRequired.toString(), asset: { address: asset.address, symbol: 'symbol' in asset ? asset.symbol : undefined, decimals: 'decimals' in asset ? asset.decimals : undefined }, type: SupportedEVMNetworks.includes(network) ? 'evm' : 'svm' }; paymentNetworks.push(networkInfo); } return server.tool(name, description, paramsSchema, { ...annotations, paymentHint: true, paymentPriceUSD: price, paymentNetworks, paymentVersion: x402Version }, (async (args, extra) => { const recipientsByNetwork = normalizeRecipients(cfg.recipient); // Build PaymentRequirements across supported networks const buildRequirements = async () => { const reqs = []; let facilitatorKinds = null; const { supported } = useFacilitator(cfg.facilitator); const networks = Object.keys(recipientsByNetwork); for (const network of networks) { const payTo = recipientsByNetwork[network]; if (!network || !payTo) continue; const atomic = processPriceToAtomicAmount(price, network); if ("error" in atomic) continue; const { maxAmountRequired, asset } = atomic; if (SupportedEVMNetworks.includes(network)) { const extra = ("eip712" in asset ? asset.eip712 : undefined); const normalizedPayTo = getAddress(String(payTo)); const normalizedAsset = getAddress(String(asset.address)); reqs.push({ scheme: "exact", network, maxAmountRequired, payTo: normalizedPayTo, asset: normalizedAsset, maxTimeoutSeconds: 300, resource: `mcp://${name}`, mimeType: "application/json", description, extra, }); continue; } if (SupportedSVMNetworks.includes(network)) { if (!facilitatorKinds) { try { facilitatorKinds = await supported(); } catch { continue; } } let feePayer; for (const kind of facilitatorKinds.kinds) { if (kind.network === network && kind.scheme === "exact") { feePayer = kind?.extra?.feePayer ?? undefined; break; } } if (!feePayer) continue; reqs.push({ scheme: "exact", network, maxAmountRequired, payTo: String(payTo), asset: String(asset.address), maxTimeoutSeconds: 300, resource: `mcp://${name}`, mimeType: "application/json", description, extra: { feePayer }, }); continue; } } return reqs; }; const accepts = await buildRequirements(); if (!accepts.length) { const payload = { x402Version, error: "PRICE_COMPUTE_FAILED" }; const err = { isError: true, _meta: { "x402/error": payload }, content: [{ type: "text", text: JSON.stringify(payload) }], }; return err; } // Get token either from MCP _meta or from header const requestInfoUnknown = extra.requestInfo; const headersUnknown = requestInfoUnknown && requestInfoUnknown.headers; const headerToken = (() => { if (!headersUnknown) return undefined; if (typeof headersUnknown.get === "function") { return headersUnknown.get("X-PAYMENT") ?? undefined; } if (typeof headersUnknown === "object" && headersUnknown !== null) { const rec = headersUnknown; const direct = rec["X-PAYMENT"] ?? rec["x-payment"]; return typeof direct === "string" ? direct : undefined; } return undefined; })(); const metaToken = (extra?._meta && extra._meta["x402/payment"]); const token = metaToken ?? headerToken; const paymentRequired = (reason = "PAYMENT_REQUIRED", extraFields = {}) => { const payload = { x402Version, error: reason, accepts, ...extraFields, }; return { isError: true, _meta: { "x402/error": payload }, content: [{ type: "text", text: JSON.stringify(payload) }], }; }; if (!token || typeof token !== "string") return paymentRequired(); // Decode & verify let decoded; try { decoded = decodeX402Payment(token); decoded.x402Version = x402Version; } catch { return paymentRequired("INVALID_PAYMENT"); } const selected = findMatchingPaymentRequirements(accepts, decoded); if (!selected) { return paymentRequired("UNABLE_TO_MATCH_PAYMENT_REQUIREMENTS"); } const vr = await verify(decoded, selected); if (!vr.isValid) { return paymentRequired(vr.invalidReason ?? "INVALID_PAYMENT", { payer: vr.payer, }); } // Execute tool let result; let failed = false; try { result = await cb(args, extra); if (result && typeof result === "object" && "isError" in result && result.isError) { failed = true; } } catch (e) { failed = true; result = { isError: true, content: [ { type: "text", text: `Tool execution failed: ${String(e)}` }, ], }; } // Settle only on success if (!failed) { try { const s = await settle(decoded, selected); if (s.success) { result._meta ??= {}; result._meta["x402/payment-response"] = { success: true, transaction: s.transaction, network: s.network, payer: s.payer, }; } else { return paymentRequired(s.errorReason ?? "SETTLEMENT_FAILED"); } } catch { return paymentRequired("SETTLEMENT_FAILED"); } } return result; })); } Object.defineProperty(server, "paidTool", { value: paidTool, writable: false, enumerable: false, configurable: true, }); return server; } //# sourceMappingURL=with-x402.js.map