APIs·
advanced
·12 min read·Apr 4, 2026·Updated Oct 10, 2026
By Rad Tome·Lead AI Systems Architect·How we verify this

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.

StripeMPPMachine Payments ProtocolpaymentsMCP servermonetizationbillingTypeScript
Config Target: claude_desktop_config.json
{
  "mcpServers": {
    "stripe": {
      "command": "npx",
      "args": [
        "-y",
        "@stripe/mcp-server-stripe"
      ],
      "env": {
        "STRIPE_SECRET_KEY": "sk_test_xxxxxxxxxxxx"
      }
    }
  }
}
Interactive Tool
1-Click Export

Generate & Test Stripe MCP Server Config

One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.

Open in Generator

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:

  1. ▸The agent calls your MCP tool. You validate the request.
  2. ▸You return a paymentLink — a plain HTTPS URL on your own server.
  3. ▸The agent POSTs to that URL with no credential.
  4. ▸Your endpoint returns 402 with a payment challenge.
  5. ▸The agent obtains a credential (a Shared Payment Token (opens in a new tab)) and retries.
  6. ▸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:

bash
curl https://api.stripe.com/v2/network/business_profiles/me \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -H "Stripe-Version: 2026-07-29.preview"

Install dependencies

bash
npm install @hono/node-server @modelcontextprotocol/sdk hono mppx stripe zod

mppx 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:

typescript
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:

typescript
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:

bash
curl https://api.stripe.com/v1/crypto/deposit_addresses \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -H "Stripe-Version: 2026-07-29.preview" \
  -d network=tempo

Store the returned address and add it to your config:

typescript
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):

typescript
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

bash
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:

bash
npx tsx --env-file=/absolute/path/to/.env /absolute/path/to/server.ts
bash
npx skills add stripe/link-cli

Then prompt it:

text
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

Technical Changelog

Weekly Protocol Diffs & Verified Drops

New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.

Zero marketing spam. 1-click unsubscribe anytime.

Ready to Deploy?

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.

Customize in Generator
Developer Verification & Feedback
0 reports

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.

RT

Written by Rad Tome

Lead AI Systems Architect & Maintainer, MCP Codex

@RadTome

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.

Editorial Integrity: Package names are checked against the npm registry and repositories against the GitHub API. Where a package could not be verified, the guide says so instead of giving you a command to run.

Related Guides