Stripe has one of the better-designed APIs in payments. The documentation is thorough, the SDKs are well-maintained, and the developer experience is genuinely good. None of that changes the fact that integrating with it is tedious work.

The API surface is enormous: hundreds of endpoints across customers, payments, subscriptions, invoices, checkout, billing, webhooks, and more. Payment flows have complex state machines (a PaymentIntent alone has eight possible states with specific transition rules). Testing requires managing test-mode keys, webhook simulation through the Stripe CLI or a local tunnel, and careful handling of 3D Secure flows, currency edge cases, and idempotency. Every integration project I have worked on involved hours of exploratory API calls, reading response shapes, and figuring out the right combination of endpoints for the specific payment flow.

That exploratory work is exactly what AI agents are good at. I was building a Stripe integration for a side project with an agent handling the discovery: proposing payment flows, testing edge cases against the test-mode API, iterating on webhook configurations. Halfway through, I noticed the agent’s conversation history contained a customer’s email address, a partial card fingerprint, and a PaymentIntent client secret. All returned by Stripe’s API, all sitting in the model context where they had no business being.

I looked for an existing MCP server that would give an agent comprehensive Stripe access with proper data handling. The official Stripe MCP server (mcp.stripe.com) uses OAuth and runs remotely. It covers a smaller toolset, includes doc search, and relies on Stripe’s infrastructure for data handling. Nothing I found covered enough of the API surface while also preventing sensitive payment data from leaking into conversation history.

So I built one. Local, comprehensive, and sanitised by default.

What it does

52 tools across 8 domains: customers, payments, subscriptions, invoices, refunds, checkout, balance, and webhooks. Plus 4 read-only MCP resources (account details, balance, webhook endpoints, product catalogue) and 4 prompt templates for common integration tasks like webhook setup and payment troubleshooting.

The comparison with the official server:

This serverStripe official
Transportstdio (local)HTTP (remote)
AuthSTRIPE_SECRET_KEY envOAuth
Tools52Smaller subset
PII redactionBuilt-inStripe-managed
Doc searchNoYes
Idempotency keysAll mutating toolsVaries
Input validationStrict schemas (Zod)Varies

The two servers are complementary. Run both if you want operational tools plus doc search.

MCP request/response flow: AI agent sends request through tool router, Stripe API response crosses trust boundary through sanitisation layer before reaching model context

Security posture: sanitise by default, not by opt-in

This is where the data engineering instinct shaped the design most directly. In pipeline work, you treat every boundary between systems as a potential data quality break. The boundary between a Stripe API response and a model context window is exactly that kind of break, except the failure mode is not a corrupted metric. It is sensitive payment data persisted in conversation logs, cached by providers, or leaked into downstream tool calls.

Every Stripe API response passes through a sanitisation layer before reaching the MCP output:

  • Secrets redacted. Webhook signing secrets, PaymentIntent client_secret values, including inside expanded nested objects.
  • PII masked. Email addresses show first two characters plus the domain. Phone numbers show the last four digits. Billing and shipping addresses are fully redacted.
  • URLs redacted. Hosted invoice URLs and invoice PDF links carry bearer-style access tokens. They never reach the model.
  • Metadata stripped. Values removed, keys preserved. An agent can see that metadata exists and what it is about without seeing the values, which often contain internal identifiers or customer notes.
  • Unknown objects reduced. Any Stripe object type without an explicit sanitisation handler gets reduced to a minimal envelope: id, object, status, and redacted: true. New object types added in future API versions are safe by default.

Sanitisation decision tree: each response field is classified and routed to redact, mask, strip, reduce, or pass through

The last point matters. A common pattern in API integrations is to pass through unknown response shapes and deal with them later. In a payment context feeding an AI model, ‘deal with it later’ means the model has already seen data it should not have. The conservative default (reduce, do not pass through) inverts that risk.

Validation derived from Stripe’s own type declarations

Input validation uses Zod schemas, which is standard. The interesting part is where the enum values come from.

Checkout payment method types, webhook event names, API versions, and balance transaction types are not hardcoded. At startup, the server reads the installed Stripe SDK’s TypeScript declaration files and extracts the union types. When you upgrade the Stripe SDK, the validators update automatically without touching the server code.

function loadStripeUnionValues(
  candidatePaths: string[],
  typeName: string,
): string[] {
  for (const relPath of candidatePaths) {
    const filePath = path.join(stripePackageRoot, relPath);
    let contents: string;
    try {
      contents = fs.readFileSync(filePath, "utf8");
    } catch {
      continue;
    }
    const match = contents.match(
      new RegExp(`type\\s+${typeName}\\s*=([\\s\\S]*?);`),
    );
    if (!match) continue;
    return [...match[1]!.matchAll(/'([^']+)'/g)].map((e) => e[1]!);
  }
  console.error(
    `stripe-mcp: ${typeName} not found. Validation disabled.`,
  );
  return [];
}

If the SDK restructures its type files in a future major version, validators degrade to allow-all with a stderr warning rather than crashing. The wildcard * webhook event is always rejected regardless of validator state, because accepting it would subscribe to every event type including future ones the integration has not been designed to handle.

This is the same principle I apply to data pipelines: derive constraints from the source system’s own definitions rather than maintaining a separate list that drifts. When the source changes, the constraint updates. When the constraint cannot update, it fails open with a visible warning rather than silently accepting invalid input.

Architecture

The server is structured around domain-specific tool modules, each registering their tools with the MCP server instance:

src/
  index.ts                # Entry, tool/resource/prompt registration
  stripe-client.ts        # SDK singleton, pinned API version
  tools/
    balance.ts            # Balance and payout tools
    checkout.ts           # Checkout Session and coupon tools
    customers.ts          # Customer CRUD and search
    invoices.ts           # Invoice lifecycle tools
    payments.ts           # PaymentIntent and PaymentMethod tools
    refunds.ts            # Refund tools
    subscriptions.ts      # Subscription, Product, Price tools
    webhooks.ts           # Webhook endpoint and event tools
  resources/index.ts      # Read-only MCP resources
  prompts/index.ts        # Prompt templates
  utils/stripe-toolkit.ts # Sanitisation, validation, errors

A few deliberate choices:

Pinned API version. The Stripe client is locked to 2026-05-27.dahlia. Unpinned API versions mean the response shape can change between deployments without any code change, which breaks sanitisation assumptions. Pinning means I control when the API version advances and can verify that sanitisation still covers the new response shapes.

Bounded runtime. Network retries are capped at 0 to 5. Request timeout is capped at 1 to 120 seconds. These bounds are enforced in the configuration layer, not left to environment variable defaults. An AI agent workflow that hangs on a Stripe API call for five minutes is worse than one that fails fast and reports the timeout.

Idempotency on all mutations. Every mutating tool accepts an optional idempotency_key. An AI agent retrying a failed payment creation should not create duplicate PaymentIntents. Stripe treats deletions as inherently idempotent, so those are excluded.

No stored state. The server holds nothing between requests beyond the Stripe SDK client singleton. All state lives in Stripe’s API. This means the server can be stopped and restarted at any point without data loss or stale state, and there is nothing to back up or migrate.

Token economics and context efficiency

Sanitisation is a security measure, but it has a useful side effect: reduced token consumption. A raw Stripe PaymentIntent response includes nested customer objects, expanded payment methods, full metadata dictionaries, and verbose address blocks. After sanitisation, that same response is significantly smaller. Secrets are gone. PII is replaced with short masked values. Metadata values are stripped entirely. Address objects collapse to [redacted].

This matters because tokens are currency, and they are getting more expensive as model providers adjust pricing. Every tool call that returns a bloated API response eats into the context window, leaving less room for reasoning, follow-up queries, and the conversation history that gives the agent context about what it has already done.

The MCP protocol does not enforce response size limits. A naive MCP server that passes through raw API responses lets the external API dictate how much of the model’s context budget each tool call consumes. A sanitised server puts that control back in the operator’s hands. The response contains exactly what the agent needs for its next decision (the payment status, the amount, the currency, the error code) and nothing it does not (the customer’s home address, the webhook signing secret, the hosted invoice URL with its embedded bearer token).

For agent workflows that chain multiple Stripe operations (create customer, create subscription, generate invoice, check payment status), the cumulative token saving across a session is substantial. Each tool call returns a smaller response, and the model’s context window stays cleaner for longer before conversation compression or summarisation is needed.

Trade-offs

No doc search. The official Stripe MCP server includes documentation search, which is useful for discovery. This server focuses on operations. If you want both, run both.

Local only. Stdio transport means the server runs on the same machine as the MCP client. No remote access, no multi-user scenarios. For a development workflow, local is a feature (no network latency, no OAuth dance, full control). For a team tool, it would need a different transport.

Enum validators are SDK-coupled. The startup validator loading depends on the Stripe SDK’s internal file structure. If Stripe changes that structure in a major SDK release, validators degrade rather than crash. But they do degrade, and the warning goes to stderr where an agent workflow might not surface it. Monitoring that degradation path is the operator’s responsibility.

TypeScript, not Python. The MCP ecosystem has SDKs in both languages. I chose TypeScript because Stripe’s type declarations are the richest in its Node SDK, and the validator derivation strategy depends on those declarations being accessible. A Python implementation would need a different approach to enum validation.

What this proves

Building MCP servers is infrastructure work for agentic AI workflows. The same engineering discipline that goes into a data pipeline (validate inputs, sanitise outputs, fail predictably, derive constraints from source systems) applies directly to building the tools that AI agents use.

The server is open source on GitHub, MIT licensed. The security model, the validation approach, and the sanitisation-by-default pattern are reusable beyond Stripe. Any MCP server that touches sensitive API data faces the same design problem: the model context is an output boundary, and output boundaries need gates.

I built a similar gate system for a multi-agent property sale campaign, where compliance hooks blocked any artefact from reaching publication without passing validation. The domain changes (payment data vs. property listings), but the pattern holds: constrain what crosses the boundary, fail loud when something unexpected arrives, and make the safe path the default path.