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