mcpay
Version:
SDK and CLI for MCPay functionality - MCP servers with payment capabilities
266 lines • 11.4 kB
JavaScript
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