Accepting Stripe's Machine Payments Protocol (MPP) in Your MCP Server
Charge agents per tool call with MPP: an MCP tool that returns a payment link, an endpoint that answers 402 with a challenge, and Shared Payment Token verification.
claude_desktop_config.json{
"mcpServers": {
"stripe": {
"command": "npx",
"args": [
"-y",
"@stripe/mcp-server-stripe"
],
"env": {
"STRIPE_SECRET_KEY": "sk_test_xxxxxxxxxxxx"
}
}
}
}Generate & Test Stripe MCP Server Config
One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.
Accepting MPP in Your MCP Server
Machine Payments Protocol (MPP) lets a seller charge an AI agent for a call without any checkout UI. It is an open protocol co-authored by Stripe and Tempo (opens in a new tab).
This guide follows Stripe's own monetize-your-MCP-server walkthrough (opens in a new tab), which describes the approach as experimental. Treat the code below as the shape of a real integration, not a stable API contract. For the client side, see Paying for MCP tools with MPP.
How payment actually works
MPP is an HTTP handshake. Your MCP tool does not collect payment itself — it returns a payment link pointing at an HTTP endpoint that speaks MPP:
- ▸The agent calls your MCP tool. You validate the request.
- ▸You return a
paymentLink— a plain HTTPS URL on your own server. - ▸The agent
POSTs to that URL with no credential. - ▸Your endpoint returns
402with a payment challenge. - ▸The agent obtains a credential (a Shared Payment Token (opens in a new tab)) and retries.
- ▸You verify payment, run your business logic, and return the result with an MPP receipt.
Pricing is expressed in that 402 challenge. There is no x-mpp-pricing field
to attach to your MCP tool schema, and no per-tool MPP middleware to install.
Before you begin
- ▸Access to Shared Payment Tokens and a valid Stripe profile.
- ▸A fulfilment flow that validates requests and completes purchases.
- ▸Three environment variables:
BASE_URL,STRIPE_SECRET_KEY,STRIPE_PROFILE_ID.
STRIPE_PROFILE_ID is your Stripe profile ID, and you pass it as the networkId.
Fetch it with:
curl https://api.stripe.com/v2/network/business_profiles/me \
-u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
-H "Stripe-Version: 2026-07-29.preview"Install dependencies
npm install @hono/node-server @modelcontextprotocol/sdk hono mppx stripe zodmppx is the protocol library that issues and verifies challenges. It is not
published under a Stripe scope; @stripe/mcp-mpp-adapter and
@stripe/mcp-mpp-server do not exist on npm.
Define your MCP tool
The tool validates the purchase and returns a payment link. It does not touch payment:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import {
WebStandardStreamableHTTPServerTransport,
} from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
import { z } from 'zod'
import { getItem, validatePurchase } from './catalog.js'
function createMcpServer(): McpServer {
const server = new McpServer({
name: 'your-mcp-server',
version: '1.0.0',
})
server.registerTool(
'create_purchase_link',
{
description:
'Returns a payment link for a one-time purchase. ' +
'POST to the link with an MPP credential to pay automatically, ' +
'or open it in a browser to preview the browser checkout fallback.',
inputSchema: {
itemId: z.string(),
quantity: z.number().int().positive(),
customerName: z.string(),
customerEmail: z.string(),
},
},
async ({ itemId, quantity, customerName, customerEmail }) => {
const params = new URLSearchParams({
itemId,
quantity: String(quantity),
customerName,
customerEmail,
})
const paymentLink = `${process.env.BASE_URL}/api/purchase?${params}`
const item = getItem(itemId)
if (!item) throw new Error('Item not found')
if (!validatePurchase({ item, quantity, customerName, customerEmail })) {
throw new Error('Invalid purchase request')
}
return {
content: [{
type: 'text',
text: JSON.stringify({
// This link must be MPP enabled
paymentLink,
instructions: {
agent: `POST to paymentLink. Server returns 402 on first call — use link-cli to obtain an SPT for networkId "${process.env.STRIPE_PROFILE_ID}" and retry.`,
browser: 'Open paymentLink in a browser to view the checkout page.',
},
item: {
title: item.title,
quantity,
price: `${(item.priceCents / 100).toFixed(2)} USD`,
},
}, null, 2),
}],
}
},
)
return server
}
export async function handleMcpRequest(request: Request): Promise<Response> {
const server = createMcpServer()
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
})
await server.connect(transport)
try {
return await transport.handleRequest(request)
} finally {
await server.close()
}
}Two details worth copying deliberately. First, the agent instruction tells the
caller what to expect (a 402) and where to get a credential, which is what
makes the flow self-describing. Second, the browser instruction exists because
the same URL has to work for humans.
Create the payment endpoint
The endpoint branches on the Accept header, then runs the MPP charge:
import crypto from 'crypto'
import StripeClient from 'stripe'
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { Mppx, stripe } from 'mppx/server'
import { completeOrder, getItem, validatePurchase } from './catalog.js'
import { handleMcpRequest } from './mcp.js'
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY environment variable is required')
}
if (!process.env.STRIPE_PROFILE_ID) {
throw new Error('STRIPE_PROFILE_ID environment variable is required')
}
if (!process.env.BASE_URL) {
throw new Error('BASE_URL environment variable is required')
}
// Secret used to secure payment challenges
// https://mpp.dev/protocol/challenges#challenge-binding
const mppSecretKey = crypto
.createHmac('sha256', process.env.STRIPE_SECRET_KEY)
.update('mpp-challenge-signing')
.digest('base64')
const stripeClient = new StripeClient(process.env.STRIPE_SECRET_KEY)
const stripeMachinePayments = stripe.create({
client: stripeClient,
networkId: process.env.STRIPE_PROFILE_ID,
livemode: !process.env.STRIPE_SECRET_KEY.includes('_test_'),
})
const mppx = Mppx.create({
methods: [stripeMachinePayments.spt.charge()],
secretKey: mppSecretKey,
})
async function handler(request: Request): Promise<Response> {
const url = new URL(request.url)
const params = url.searchParams
// Browser redirect — replace with your real checkout flow
const accept = request.headers.get('Accept') ?? ''
if (accept.includes('text/html')) {
return Response.redirect(new URL(
`/checkout/${params.get('itemId')}?${params}`,
process.env.BASE_URL,
))
}
// Agent/programmatic path — MPP 402-challenge flow
const item = getItem(params.get('itemId'))
if (!item) return Response.json({ error: 'Not found' }, { status: 404 })
const quantity = Number(params.get('quantity'))
const customerName = params.get('customerName')
const customerEmail = params.get('customerEmail')
const purchase = { item, quantity, customerName, customerEmail }
if (!validatePurchase(purchase)) {
return Response.json({ error: 'Invalid purchase request' }, { status: 400 })
}
const result = await mppx.compose([
'stripe/charge',
{
amount: ((purchase.item.priceCents * purchase.quantity) / 100).toFixed(2),
currency: 'usd',
decimals: 2,
description: purchase.item.title,
},
])(request)
if (result.status === 402) return result.challenge
// Payment confirmed — run your business logic
const order = completeOrder(purchase)
return result.withReceipt(Response.json({ success: true, orderId: order.id }))
}
const app = new Hono()
app.post('/mcp', (context) => handleMcpRequest(context.req.raw))
app.on(['GET', 'POST'], '/api/purchase', (context) => handler(context.req.raw))
app.get('/checkout/:itemId', (context) =>
context.text('Replace this route with the browser checkout flow for your application.'),
)
serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 4242) })mppx.charge({ amount: '0.50' }) is the shorthand when the price is fixed.
mppx.compose() is what you want when the amount depends on the request.
Idempotency is your problem, and it is a real one
The agent can retry. If the connection drops after you charge but before the response lands, the client has no idea whether you were paid, and it will try again. Stripe's guidance is explicit: make your fulfilment logic idempotent so a retry cannot double-charge or corrupt state. Key your fulfilment on a value you control from the challenge, not on the request count.
Accept stablecoin payments too
The default spt.charge() covers cards, with a 0.50 USD minimum. To support
agents with crypto wallets you need a Tempo deposit address. Stripe offramps it
and settles into your Stripe balance:
curl https://api.stripe.com/v1/crypto/deposit_addresses \
-u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
-H "Stripe-Version: 2026-07-29.preview" \
-d network=tempoStore the returned address and add it to your config:
const stripeMachinePayments = stripe.create({
client: stripeClient,
networkId: process.env.STRIPE_PROFILE_ID,
livemode: !process.env.STRIPE_SECRET_KEY.includes('_test_'),
depositAddresses: {
tempo: process.env.TEMPO_DEPOSIT_ADDRESS!,
},
});No other code changes are needed — defaultMethods() adds Tempo, and the same
charge amount applies. Stablecoin minimums are 0.01 USDC, so sub-cent pricing
only works on this rail.
Sponsor gas
To remove your customers' network fees, set hostedFeePayer: true (requires
mppx 0.9.2 or later):
const stripeMachinePayments = stripe.create({
client: stripeClient,
networkId: process.env.STRIPE_PROFILE_ID,
livemode: true,
hostedFeePayer: true,
depositAddresses: {
tempo: process.env.TEMPO_DEPOSIT_ADDRESS!,
},
});Stripe covers the fees itself. It applies to live-mode Tempo integrations only, the deposit address and API key must belong to the same account, and it cannot be combined with Connect.
Test it
Automated
npx mppx@latest validate Stripe recommends running validate against both sandbox and live versions. It
checks discovery, challenge format, error handling, and the full payment flow.
End to end with a real agent
Start the server, register it with your harness, add the Link agent wallet skill, and tell the agent you are in test mode:
npx tsx --env-file=/absolute/path/to/.env /absolute/path/to/server.tsnpx skills add stripe/link-cliThen prompt it:
Use the `create_purchase_link` tool from the paid-catalog MCP to purchase one
item with the ID `coffee` for a customer named `Alice` with the email
`test@example.com`. This flow uses test mode, so it doesn't move real funds.If it works end to end, the agent discovered the tool, called the endpoint, got the 402, obtained a sandbox credential, and completed the purchase unaided.
List yourself in the Stripe Directory
Once it works, Stripe accepts listings for agent-discoverable services. Email
machine-payments@stripe.com with your business name, Stripe account ID,
Stripe profile ID, a link to your llms.txt and any agent skills, and one to
three example prompts.
Reference
- ▸MPP specification (opens in a new tab)
- ▸Stripe: monetize your MCP server (opens in a new tab)
- ▸Stripe: MPP (opens in a new tab)
- ▸Stripe: machine payments (opens in a new tab)
- ▸Stripe: Shared Payment Tokens (opens in a new tab)
- ▸Stripe sample app: machine-payments (opens in a new tab)
- ▸mppx on npm (opens in a new tab)
Weekly Protocol Diffs & Verified Drops
New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.
Zero marketing spam. 1-click unsubscribe anytime.
Build your full agent toolstack in the Visual Generator
Combine Accepting Stripe's Machine Payments Protocol (MPP) in Your with databases, search APIs, and memory graphs in a single configuration file.
Did this setup guide work with your AI host?
Help test this configuration across Claude, Cursor, and Codex CLI. Be the first to report:
Accepting Stripe's Machine Payments Protocol (MPP) in Your MCP Server FAQ
What is the Accepting Stripe's Machine Payments Protocol (MPP) in Your MCP Server?
Charge agents per tool call with MPP: an MCP tool that returns a payment link, an endpoint that answers 402 with a challenge, and Shared Payment Token verification.
How do I configure Accepting Stripe's Machine Payments Protocol (MPP) in Your MCP Server in Claude Desktop or Cursor?
You can copy the configuration JSON from our guide or launch the interactive MCP Codex Config Generator at https://mcp-codex.com/generator to export valid configs in 1 click.
Can I use Accepting Stripe's Machine Payments Protocol (MPP) in Your MCP Server with the OpenAI Codex CLI?
Yes, OpenAI Codex CLI supports Model Context Protocol. You can add it directly to ~/.codex/config.toml or pass arguments to codex mcp add.
Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.
Related Guides
HubSpot and Zendesk MCP Integration Guide
Connect HubSpot CRM contacts and Zendesk tickets to Model Context Protocol agents: Automated ticket triage, contact enrichment, and safe draft workflows.
APIsAlpaca Finance API MCP Server Guide
Connect Alpaca Finance API to Model Context Protocol: Stream real-time market data, execute paper trades safely, and enforce autonomous budget guardrails.
APIsPaying for MCP Tools with Stripe's Machine Payments Protocol (MPP)
How an MCP client pays for a tool call with MPP: the HTTP 402 challenge, Shared Payment Tokens from a Link agent wallet, and the stablecoin alternative.