Skip to main content
Glama
tas1337

MCP A2A AP2 Food Delivery & Payments

by tas1337
ap2-tools.ts8.33 kB
// ============================================================================= // AP2 TOOLS - PAYMENTS WITH MANDATES // ============================================================================= // Payment tools that use A2A to call Stripe agent. // // KEY DIFFERENCE FROM A2A: // - A2A (food tools): Agent freely calls other agents // - AP2 (payment tools): Agent MUST have user-signed mandate first // // FLOW: // 1. AI asks user: "Should I place this order for $15.37?" // 2. User says: "Yes" // 3. Agent calls requestUserAuthorization() → user-authorization.ts creates mandate // 4. Agent verifies mandate is valid (signature, not expired, amount ok) // 5. Agent sends payment request WITH mandate to Stripe agent // 6. Stripe agent ALSO verifies mandate before processing // // WHY MANDATES? // - Authorization: Proves user approved this specific payment // - Authenticity: Signature proves it wasn't tampered with // - Accountability: Clear audit trail of who approved what // ============================================================================= import { createHmac } from 'crypto'; import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import type { Mandate, MandateRequest, MandatePayload, MandateVerification, PaymentResponse, PaymentStatusResponse, PaymentStatusParams, RefundResponse, PaymentRequestWithMandate, RefundRequestWithMandate, PaymentResponseWithMandate, RefundResponseWithMandate, ProcessPaymentArgs, ProcessRefundArgs, } from './interfaces.js'; import type { PaymentMethod } from './types.js'; import { discoverAgents, callAgent } from './a2a-client.js'; import { authorizePayment } from './user-authorization.js'; const USER_SECRET = process.env.USER_SECRET || 'user-secret-key'; // Must match user-authorization.ts // ============================================================================= // MANDATE FUNCTIONS // ============================================================================= function createMandatePayload(m: MandatePayload): string { return JSON.stringify({ userId: m.userId, agentId: m.agentId, action: m.action, maxAmount: m.maxAmount, currency: m.currency, orderId: m.orderId, issuedAt: m.issuedAt, expiresAt: m.expiresAt, }); } // Calls the User Wallet Service to get authorization // In real world: This triggers Face ID / PIN on user's phone async function requestUserAuthorization(req: MandateRequest): Promise<Mandate> { return authorizePayment(req); } // Verify mandate before sending to Stripe (defense in depth) // Stripe agent ALSO verifies - double check is intentional function verifyMandate(m: Mandate, amount?: number): MandateVerification { const payload = createMandatePayload(m); const expected = createHmac('sha256', USER_SECRET).update(payload).digest('hex'); // Check 1: Signature valid? (proves user signed it, not tampered) if (m.signature !== expected) { return { valid: false, reason: 'Bad signature' }; } // Check 2: Not expired? (mandates only valid for 5 min) if (new Date() > new Date(m.expiresAt)) { return { valid: false, reason: 'Expired' }; } // Check 3: Amount within limit? (can't charge more than user approved) if (amount !== undefined && amount > m.maxAmount) { return { valid: false, reason: 'Over limit' }; } return { valid: true }; } // ============================================================================= // TOOL DEFINITIONS // ============================================================================= export const processPaymentTool: Tool = { name: 'process_payment', description: 'Process payment via Stripe. Requests user authorization, then sends mandate to Stripe.', inputSchema: { type: 'object', properties: { amount: { type: 'number', description: 'Payment amount' }, currency: { type: 'string', description: 'Currency code (e.g., USD)' }, paymentMethod: { type: 'string', description: 'Payment method', enum: ['credit_card', 'debit_card', 'paypal', 'apple_pay', 'google_pay', 'digital_wallet'], }, orderId: { type: 'string', description: 'Order ID' }, userId: { type: 'string', description: 'User ID for mandate' }, }, required: ['amount', 'currency', 'paymentMethod', 'orderId', 'userId'], }, }; export const checkPaymentStatusTool: Tool = { name: 'check_payment_status', description: 'Check payment status. No mandate needed.', inputSchema: { type: 'object', properties: { transactionId: { type: 'string', description: 'Transaction ID' }, }, required: ['transactionId'], }, }; export const processRefundTool: Tool = { name: 'process_refund', description: 'Process refund via Stripe. Requests user authorization, then sends mandate to Stripe.', inputSchema: { type: 'object', properties: { transactionId: { type: 'string', description: 'Transaction ID to refund' }, orderId: { type: 'string', description: 'Order ID' }, userId: { type: 'string', description: 'User ID for mandate' }, amount: { type: 'number', description: 'Refund amount (optional, defaults to full)' }, reason: { type: 'string', description: 'Refund reason (optional)' }, }, required: ['transactionId', 'orderId', 'userId'], }, }; // ============================================================================= // HANDLERS // ============================================================================= // Called by place_order AFTER user said "yes" in chat export async function handleProcessPayment(args: ProcessPaymentArgs): Promise<PaymentResponseWithMandate> { // Step 1: Request authorization from User Wallet Service // This calls the separate wallet container, which simulates Face ID / PIN const mandate = await requestUserAuthorization({ userId: args.userId, agentId: 'mcp-server', action: 'payment', maxAmount: args.amount, currency: args.currency, orderId: args.orderId, }); // Step 2: Verify mandate before sending (our check) const verification = verifyMandate(mandate, args.amount); if (!verification.valid) { throw new Error(`Mandate failed: ${verification.reason}`); } // Step 3: Find Stripe agent via A2A const agents = await discoverAgents('process_payment'); if (!agents.length) { throw new Error('No payment agent available'); } // Step 4: Send payment WITH mandate to Stripe agent // Stripe will ALSO verify the mandate before processing const request: PaymentRequestWithMandate = { amount: args.amount, currency: args.currency, paymentMethod: args.paymentMethod as PaymentMethod, orderId: args.orderId, mandate, // <-- THIS is the proof of user approval }; const response = await callAgent<PaymentRequestWithMandate, PaymentResponse>( agents[0], 'payment/process', request ); return { ...response, mandate }; } export async function handleCheckPaymentStatus(args: PaymentStatusParams): Promise<PaymentStatusResponse> { const agents = await discoverAgents('process_payment'); if (!agents.length) { throw new Error('No payment agent available'); } return callAgent<PaymentStatusParams, PaymentStatusResponse>( agents[0], 'payment/status', { transactionId: args.transactionId } ); } export async function handleProcessRefund(args: ProcessRefundArgs): Promise<RefundResponseWithMandate> { // Request authorization from User Wallet Service const mandate = await requestUserAuthorization({ userId: args.userId, agentId: 'mcp-server', action: 'refund', maxAmount: args.amount || 999999, currency: 'USD', orderId: args.orderId, }); const verification = verifyMandate(mandate); if (!verification.valid) { throw new Error(`Mandate failed: ${verification.reason}`); } const agents = await discoverAgents('process_payment'); if (!agents.length) { throw new Error('No payment agent available'); } const request: RefundRequestWithMandate = { transactionId: args.transactionId, amount: args.amount, reason: args.reason, mandate, }; const response = await callAgent<RefundRequestWithMandate, RefundResponse>( agents[0], 'payment/refund', request ); return { ...response, mandate }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tas1337/mcp-a2a-ap2-im-hungry'

If you have feedback or need assistance with the MCP directory API, please join our Discord server