UNPKG

mcpay

Version:

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

335 lines 12.8 kB
import { SupportedEVMNetworks, SupportedSVMNetworks } from "x402/types"; import { decodePayment as decodeX402Payment } from "x402/schemes"; import { findMatchingPaymentRequirements, processPriceToAtomicAmount } from "x402/shared"; import { useFacilitator } from "x402/verify"; import { getAddress } from "viem"; export class X402MonetizationHook { name = "x402-monetization"; cfg; verify; settle; x402Version; constructor(cfg) { this.cfg = cfg; const { verify, settle } = useFacilitator(cfg.facilitator); this.verify = verify; this.settle = settle; this.x402Version = cfg.version ?? 1; } normalizeRecipients(r) { if (!r || typeof r !== "object") return {}; const out = {}; try { } catch { } const isTestnetNetwork = (network) => network.includes("sepolia") || network.includes("fuji") || network.includes("devnet") || network.includes("testnet") || network.includes("amoy"); const maybeFamily = r; if (maybeFamily.evm?.address) { const useTestnet = maybeFamily.evm.isTestnet; for (const net of SupportedEVMNetworks) { if (useTestnet === undefined || isTestnetNetwork(net) === !!useTestnet) { out[net] = maybeFamily.evm.address; } } } if (maybeFamily.svm?.address) { const useTestnet = maybeFamily.svm.isTestnet; for (const net of SupportedSVMNetworks) { if (useTestnet === undefined || isTestnetNetwork(net) === !!useTestnet) { out[net] = maybeFamily.svm.address; } } } const allKnown = new Set([...SupportedEVMNetworks, ...SupportedSVMNetworks]); for (const [key, value] of Object.entries(r)) { if (typeof value === "string" && allKnown.has(key)) { out[key] = value; } } try { } catch { } return out; } async buildRequirements(toolName, description, price) { try { } catch { } const recipientsByNetwork = this.normalizeRecipients(this.cfg.recipient); const reqs = []; let facilitatorKinds = null; const networks = Object.keys(recipientsByNetwork); for (const network of networks) { const payTo = recipientsByNetwork[network]; if (!network || !payTo) continue; try { } catch { } const atomic = processPriceToAtomicAmount(price, network); if ("error" in atomic) { try { } catch { } continue; } const { maxAmountRequired, asset } = atomic; try { } catch { } 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://${toolName}`, mimeType: "application/json", description, extra, }); continue; } if (SupportedSVMNetworks.includes(network)) { if (!facilitatorKinds) { try { const { supported } = useFacilitator(this.cfg.facilitator); 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://${toolName}`, mimeType: "application/json", description, extra: { feePayer }, }); continue; } } try { } catch { } return reqs; } paymentRequired(accepts, reason, extraFields = {}) { const payload = { x402Version: this.x402Version, error: reason, accepts, ...extraFields }; try { } catch { } return { isError: true, _meta: { "x402/error": payload }, content: [{ type: "text", text: JSON.stringify(payload) }], }; } async processCallToolRequest(req, extra) { const name = String(req?.params?.name ?? ""); try { } catch { } if (!name) { try { } catch { } return { resultType: "continue", request: req }; } const price = this.cfg.prices[name]; if (!price) { try { } catch { } return { resultType: "continue", request: req }; } const description = `Paid access to ${name}`; const accepts = await this.buildRequirements(name, description, price); if (!accepts.length) { try { } catch { } return { resultType: "respond", response: this.paymentRequired(accepts, "PRICE_COMPUTE_FAILED") }; } const params = (req.params ?? {}); const meta = params._meta ?? {}; const token = typeof meta["x402/payment"] === "string" ? meta["x402/payment"] : undefined; if (!token) { try { } catch { } return { resultType: "respond", response: this.paymentRequired(accepts, "PAYMENT_REQUIRED") }; } let decoded; try { decoded = decodeX402Payment(token); decoded.x402Version = this.x402Version; } catch { try { } catch { } return { resultType: "respond", response: this.paymentRequired(accepts, "INVALID_PAYMENT") }; } const selected = findMatchingPaymentRequirements(accepts, decoded); if (!selected) { try { } catch { } return { resultType: "respond", response: this.paymentRequired(accepts, "UNABLE_TO_MATCH_PAYMENT_REQUIREMENTS") }; } const vr = await this.verify(decoded, selected); if (!vr.isValid) { try { } catch { } return { resultType: "respond", response: this.paymentRequired(accepts, vr.invalidReason ?? "INVALID_PAYMENT", { payer: vr.payer }), }; } try { } catch { } return { resultType: "continue", request: req }; } async processCallToolResult(res, original, extra) { // Recompute payment context statelessly from the original request const name = String(original?.params?.name ?? ""); const price = name ? this.cfg.prices[name] : undefined; const params = (original.params ?? {}); const meta = params._meta ?? {}; const token = typeof meta["x402/payment"] === "string" ? meta["x402/payment"] : undefined; try { } catch { } // If not a paid tool or missing token, pass through if (!name || !price || !token) return { resultType: "continue", response: res }; const failed = !!res?.isError || (Array.isArray(res?.content) && res.content.length === 1 && typeof res.content[0]?.text === "string" && res.content[0].text.includes("error")); if (failed) { try { } catch { } return { resultType: "continue", response: res }; } try { // Rebuild requirements and selected match const accepts = await this.buildRequirements(name, `Paid access to ${name}`, price); let decoded; try { decoded = decodeX402Payment(token); decoded.x402Version = this.x402Version; } catch { try { } catch { } return { resultType: "continue", response: res }; } const selected = findMatchingPaymentRequirements(accepts, decoded); if (!selected) { try { } catch { } return { resultType: "continue", response: res }; } const s = await this.settle(decoded, selected); if (s.success) { try { } catch { } const meta = (res._meta ?? {}); meta["x402/payment-response"] = { success: true, transaction: s.transaction, network: s.network, payer: s.payer, }; const content = []; if (Array.isArray(res.content)) { content.push(...res.content); } const note = `Payment settled on ${s.network} (tx: ${s.transaction ?? "n/a"}).`; content.push({ type: "text", text: note }); const response = { ...res, _meta: meta, content }; return { resultType: "continue", response }; } try { } catch { } const response = this.paymentRequired([], s.errorReason ?? "SETTLEMENT_FAILED"); return { resultType: "continue", response }; } catch { try { } catch { } const response = this.paymentRequired([], "SETTLEMENT_FAILED"); return { resultType: "continue", response }; } } async processListToolsResult(result, originalRequest, extra) { // Add payment annotations to tools that have prices configured if (result.tools) { result.tools = result.tools.map((tool) => { const price = this.cfg.prices[tool.name]; if (!price) { return tool; } // Build payment network information const recipientsByNetwork = this.normalizeRecipients(this.cfg.recipient); const paymentNetworks = []; 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 { ...tool, annotations: { ...tool.annotations, paymentHint: true, paymentPriceUSD: price, paymentNetworks, paymentVersion: this.x402Version } }; }); } return { resultType: "continue", response: result }; } } //# sourceMappingURL=x402-hook.js.map