@thorium-dev-group/x402-mcp-extension
Version:
X402-MCP Protocol Extension
205 lines • 8.85 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.X402MCPClient = void 0;
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
const schemes_1 = require("x402/schemes");
const shared_1 = require("x402/shared");
const services_1 = require("../shared/services");
const error_codes_1 = require("../shared/error-codes");
const schemas_1 = require("../shared/schemas");
const viem_1 = require("viem");
const PaymentError_1 = require("../shared/errors/PaymentError");
const PaymentAuditStore_1 = require("./PaymentAuditStore");
class GuardrailsService {
maxPaymentPerCall;
whitelistedServers;
log;
constructor(opts) {
this.maxPaymentPerCall = opts.guardrails?.maxPaymentPerCall;
this.whitelistedServers = opts.guardrails?.whitelistedServers;
this.log = opts.loggerFactory.createLogger(GuardrailsService.name);
}
async enforce({ amount, payTo }) {
if (this.maxPaymentPerCall !== undefined && amount > this.maxPaymentPerCall) {
this.log.error('Payment exceeds max per-call limit', {
amountRequested: amount,
maxPaymentPerCall: this.maxPaymentPerCall
});
throw new PaymentError_1.PaymentError(error_codes_1.ERROR_CODES.GUARDRAIL_VIOLATION, `Payment exceeds max per-call limit: $${amount} > $${this.maxPaymentPerCall}`, { amount, maxPaymentPerCall: this.maxPaymentPerCall });
}
if (this.whitelistedServers && !this.whitelistedServers.includes(payTo)) {
this.log.error('Payment recipient is not whitelisted', {
payTo,
whitelistedServers: this.whitelistedServers
});
throw new PaymentError_1.PaymentError(error_codes_1.ERROR_CODES.WHITELIST_VIOLATION, `Payment recipient ${payTo} is not whitelisted`, { payTo, whitelistedServers: this.whitelistedServers });
}
}
}
class X402MCPClient extends index_js_1.Client {
walletProvider;
guardrailsService;
auditStorage;
logger;
constructor(options) {
super({
name: options.name,
version: options.version,
}, {
capabilities: {
...options.capabilities,
experimental: {
x402: {
paymentsEnabled: true,
},
}
}
});
if (typeof options.wallet === 'string') {
const { X402PKWalletProvider } = require('./x402PKWalletProvider');
this.walletProvider = new X402PKWalletProvider(options.wallet);
}
else {
this.walletProvider = options.wallet;
}
if (!options.logFactory) {
options.logFactory = new services_1.ConsoleLoggerFactory();
}
if (options.logFactory) {
this.logger = options.logFactory.createLogger(X402MCPClient.name);
}
else {
const loggerFactory = new services_1.ConsoleLoggerFactory();
this.logger = loggerFactory.createLogger(X402MCPClient.name);
}
if (options.guardrails) {
this.guardrailsService = new GuardrailsService({
guardrails: options.guardrails,
loggerFactory: options.logFactory,
});
}
if (options.auditStorage) {
this.auditStorage = new PaymentAuditStore_1.PaymentAuditStorage({
storage: options.auditStorage
});
}
this.setRequestHandler(schemas_1.PaymentRequiredRequestSchema, this.handlePaymentRequired.bind(this));
this.setNotificationHandler(schemas_1.PaymentResultNotificationSchema, this.handleNotification.bind(this));
}
async connect(transport) {
if (this.auditStorage) {
const originalSend = transport.send.bind(transport);
const serverUrl = transport.url;
transport.send = async (message, options) => {
const requestId = message.id;
const method = message.method;
const params = message.params;
if (requestId && method) {
await this.trackRequest(requestId, serverUrl, method, params);
}
try {
const result = await originalSend(message, options);
if (requestId) {
await this.markRequestCompleted(requestId);
}
return result;
}
catch (error) {
this.logger.error('Error sending request', {
operation: 'send_request',
error: error
});
if (requestId) {
await this.markRequestCompleted(requestId);
}
throw error;
}
};
}
return super.connect(transport);
}
async trackRequest(requestId, serverId, method, params) {
if (this.auditStorage) {
await this.auditStorage.storePendingRequest({
requestId,
serverId,
method,
params,
});
}
}
async markRequestCompleted(requestId) {
if (this.auditStorage) {
await this.auditStorage.markRequestCompleted(requestId);
}
}
async handlePaymentRequired(request) {
const params = request.params;
if (!params || !params.payTo || !params.maxAmountRequired || !params.network) {
throw new Error('Invalid payment_required request: missing required fields');
}
if (request.params.scheme !== 'exact') {
throw new Error('Unsupported scheme: ' + request.params.scheme);
}
if (request.params.x402Version !== 1) {
throw new Error('Unsupported x402 version: ' + request.params.x402Version);
}
const requestId = params.requestId;
if (!requestId) {
throw new PaymentError_1.PaymentError(error_codes_1.ERROR_CODES.PAYMENT_INVALID, 'Payment request missing requestId');
}
let matchingRequest = null;
if (this.auditStorage) {
matchingRequest = await this.auditStorage.getPendingRequest(requestId);
if (!matchingRequest) {
throw new PaymentError_1.PaymentError(error_codes_1.ERROR_CODES.PAYMENT_INVALID, `Payment request for unknown payment: ${requestId}`);
}
this.logger.debug('Making payment for request:', matchingRequest, params);
await this.auditStorage.updatePaymentStatus(requestId, 'pending', {
completedAt: new Date(),
});
}
if (this.guardrailsService) {
const atomicAmount = (0, shared_1.processPriceToAtomicAmount)(params.maxAmountRequired, params.network);
if ('error' in atomicAmount) {
this.logger.error('Failed to process payment amount', {
operation: 'handle_payment_required',
error: atomicAmount.error
});
throw new PaymentError_1.PaymentError(error_codes_1.ERROR_CODES.PAYMENT_INVALID, `Failed to process payment amount: ${atomicAmount.error}`);
}
const usdAmount = parseFloat((0, viem_1.formatUnits)(BigInt(params.maxAmountRequired), atomicAmount.asset.decimals));
await this.guardrailsService.enforce({
amount: usdAmount,
payTo: params.payTo,
});
}
const account = await this.walletProvider.createAccount();
const paymentPayload = await schemes_1.exact.evm.createPayment(account, params.x402Version, params);
const response = {
jsonrpc: '2.0',
id: request.id,
result: {
payment: paymentPayload,
},
};
return response;
}
async handleNotification(notification) {
const params = notification.params;
if (params.requestId && this.auditStorage) {
const matchingRequest = await this.auditStorage.getPendingRequest(params.requestId);
if (matchingRequest) {
await this.auditStorage.updatePaymentStatus(matchingRequest.requestId, params.success ? 'completed' : 'failed', {
transactionHash: params.transaction,
payerAddress: params.payer,
errorReason: params.errorReason,
completedAt: new Date(),
});
}
}
this.logger.debug('Received payment result notification:', notification);
}
}
exports.X402MCPClient = X402MCPClient;
//# sourceMappingURL=X402MCPClient.js.map