Agents & Orchestration

Actions with MCP and Stripe

Let a guarded book agent request a Stripe test-mode checkout link through MCP, with a human approval step standing between the model and the payment call.

Build it with an agent

Paste into Claude Code, Cursor, or any coding agent — it builds the recipe for you.

Start in an empty directory. Clone `https://github.com/nebius/nebius-partner-cookbook` and work inside `cookbooks/09-actions-with-mcp-stripe`. Read its `README.md`, `app/core/approvals.py`, and `app/core/stripe_mcp.py` before running anything.

Install the dependencies with `uv sync`. Read the Nebius credential from `NEBIUS_API_KEY`, or from a `.env` file in that directory if the variable is not set. If neither is present, stop and ask the user for their Nebius API key, then write it to `.env` as `NEBIUS_API_KEY=<key>` before running.

This recipe also needs two Stripe keys: `STRIPE_SECRET_KEY` for the one-time catalog seed step, and `STRIPE_MCP_API_KEY` for the running app's calls to the remote Stripe MCP server. Read both from the environment or from `.env`. If either is missing, stop and ask the user for a Stripe Sandbox or test-mode key (never a live key), and write it to `.env` before running. Do not search elsewhere on the machine for any key, and never print or commit a key.

Copy `.env.example` to `.env` and fill in the three keys above. Start Postgres with `docker compose up -d postgres`.

Run the tests offline first with `make test`, which mocks Nebius, Stripe REST, and Stripe MCP with `respx` and makes no network calls. Report whether they pass.

Seed the Stripe test catalog with `make seed-stripe-books`, then start the service with `make dev`.

Exercise the primary endpoint with the README's curl example:

```bash
curl -N -X POST http://localhost:8000/agent/run \
  -H 'content-type: application/json' \
  -d '{"thread_id":"checkout-demo","user_id":"reader-42","prompt":"I want to buy The Nebius Cloud Atlas."}'
```

Report the SSE events you see, in particular whether an `approval_required` event appears before any Stripe call. Then approve the pending action with:

```bash
curl -X POST http://localhost:8000/approvals/$APPROVAL_ID \
  -H 'content-type: application/json' \
  -d '{"decision":"approve"}'
```

using the `approvalId` from the SSE stream. Report whether the response includes a Stripe checkout URL, and confirm from the Stripe Sandbox dashboard that the resulting Payment Link and Product were created in test mode, not live mode.

Before starting the service, set `NEBIUS_BASE_URL=https://api.tokenfactory.nebius.com/v1/` and `NEBIUS_MODEL=Qwen/Qwen3.5-397B-A17B` in `.env`; the repository's `.env.example` still carries the older `api.studio.nebius.ai` hostname and a chat model that Token Factory no longer serves.

Recipe

Let a guarded book agent create a Stripe test-mode checkout link, but only after explicit user approval.

This cookbook builds on the guarded memory agent from earlier recipes in the same arc and adds one production capability: the agent can propose an external action. The model never gets uncontrolled authority. It can request a checkout action, but the FastAPI service owns approval, policy, audit events, and the Stripe MCP call.

What you will build

A FastAPI service that extends the guarded memory agent with:

  1. The inherited agent stack. LangGraph routing, thread memory, Postgres user memory, LangSmith tracing, guardrails, server-sent events, and metrics stay in place.
  2. A seeded book catalog. Ten fictional books live in scripts/data/books.json, with illustration paths ready for your assets.
  3. A Stripe setup script. make seed-stripe-books creates Stripe test-mode Products and Prices through the Stripe REST API and writes data/stripe_books.json.
  4. A remote Stripe MCP runtime. Approved actions call https://mcp.stripe.com with the create_payment_link tool.
  5. Human approval. /agent/run emits an approval_required event, and /approvals/{approval_id} approves or rejects the side effect.
book purchase prompt
  └─► input guardrails
      └─► select seeded book
          └─► create pending approval
              ├─► reject: no Stripe call
              └─► approve: Stripe MCP create_payment_link

Prerequisites

  • Python 3.12 or newer
  • uv installed
  • A Nebius API key
  • A Stripe Sandbox or test-mode account
  • A Stripe test-mode secret key for seeding
  • A restricted Stripe key for remote MCP runtime calls
  • Docker, for local Postgres

Run the cookbook

cd cookbooks/09-actions-with-mcp-stripe
uv sync
cp .env.example .env

Fill these values in .env:

NEBIUS_API_KEY=...
NEBIUS_BASE_URL=https://api.tokenfactory.nebius.com/v1/
NEBIUS_MODEL=Qwen/Qwen3.5-397B-A17B
STRIPE_SECRET_KEY=sk_test_or_sandbox_seed_key
STRIPE_MCP_API_KEY=rk_test_or_sandbox_restricted_key

The two NEBIUS_ overrides matter: the repo’s .env.example still ships the older api.studio.nebius.ai hostname and a chat model that Token Factory no longer serves, so the first request fails without them.

Start Postgres for the inherited long-term memory:

docker compose up -d postgres

Create the Stripe test catalog:

make seed-stripe-books

Run the app:

make dev

In the web playground, open this recipe and ask:

I want to buy The Nebius Cloud Atlas.

The assistant emits an approval card. Approving it calls Stripe MCP and returns a test checkout URL.

Environment

Variable Default Purpose
NEBIUS_API_KEY required Nebius API key.
STRIPE_SECRET_KEY sk_test_replace_me Setup-only key used by make seed-stripe-books.
STRIPE_MCP_API_KEY rk_test_replace_me Runtime key used to call remote Stripe MCP.
STRIPE_MCP_BASE_URL https://mcp.stripe.com Remote Stripe MCP endpoint.
BOOK_CATALOG_PATH data/stripe_books.json Generated catalog with Stripe Product and Price IDs.
APPROVAL_TTL_SECONDS 900 Time window for approving a pending action.
MEMORY_BACKEND postgres Inherited long-term memory backend.
POSTGRESQL_ADDON_URI local Postgres URL Inherited memory database.
LANGSMITH_TRACING false Enables LangSmith SaaS traces when credentials are configured.

NEBIUS_BASE_URL and NEBIUS_MODEL are not in this table because the quickstart already tells you to set them: the repo’s .env.example and config default still point at the retired api.studio.nebius.ai host and model.

Walk-through

Stripe MCP and sandboxes

Stripe MCP exposes Stripe API operations as MCP tools, including create_payment_link. This cookbook uses the remote MCP endpoint instead of a local npx @stripe/mcp subprocess, so the Python service stays simple to run and easy to mock in tests.

Use a Stripe Sandbox or test-mode account. Never use live keys for this cookbook.

Stripe Sandboxes are newer than the classic global test-mode toggle. A Sandbox is an isolated Stripe environment with its own API keys, objects, settings, and test data, which is useful for agent work: you can give the agent a realistic account shape without letting it touch your real Stripe account or pollute a shared test-mode setup.

Classic test mode is still enough for local experimentation. A Sandbox is the stronger choice for repeatable demos, CI smoke tests, or multiple isolated agent environments. The recommended setup is:

  • one Sandbox dedicated to the cookbook demo;
  • one setup key for creating the fake Products and Prices;
  • one restricted runtime key for the remote MCP server;
  • no live-mode objects, prices, customers, or checkout links.

This separation mirrors a production control boundary. Operators prepare the commerce catalog, and the agent can only request a narrow runtime action after the user approves it.

Recommended key model:

  • Use STRIPE_SECRET_KEY only for the operator seed step.
  • Use STRIPE_MCP_API_KEY for the running app.
  • Prefer a restricted key for MCP that can create Payment Links from existing Prices and cannot perform broad account operations.
  • Rotate both keys after demos or CI smoke tests.

The seed script uses Stripe REST instead of MCP on purpose. Seeding is an operator task: create Products and multi-currency Prices once, then let the runtime agent use MCP only for the approved action. Each book gets one Stripe Price with USD as the default currency and EUR, GBP, and SGD configured through Stripe currency_options.

Why Stripe MCP here

Stripe is a good action for this kind of cookbook because it is concrete, high stakes, and easy to keep safe in test mode. Creating a checkout link is visibly different from answering a question, and the approval pattern stays clear: payment-related side effects should never happen just because the model suggested them.

Other MCP servers could participate in the same flow:

  • Catalog or CMS MCP to fetch approved book metadata before checkout.
  • Inventory MCP to confirm a book or bundle is available.
  • CRM MCP to associate the approved action with a demo user or account.
  • Email MCP to send the checkout link after approval.
  • Ticketing or audit MCP to record the approval trail for compliance.
  • Knowledge-base MCP to fetch refund, tax, or shipping policy before the agent explains the checkout.

The pattern stays the same regardless of the MCP provider:

model proposes action → backend validates policy → user approves → MCP tool executes

Tool discovery is not tool authority. MCP gives the agent a standardized way to reach tools, but the application still decides which tools are allowed, what arguments are valid, who can approve them, and what gets logged.

Seed catalog

The source catalog is committed at scripts/data/books.json. It contains ten fictional books that get created in the Stripe Sandbox. Each book uses a single Stripe Price with USD as the default and EUR, GBP, and SGD as multi-currency options:

Book Author ISBN-13 USD EUR GBP SGD
The Nebius Cloud Atlas Nia Vector 9781600000010 $14.99 €13.99 £11.99 S$19.99
Pinecones in the Vector Garden Ada Embedding 9781600000027 $12.99 €11.99 £10.99 S$17.99
Agent at the End of the Prompt Lila Chain 9781600000034 $10.99 €9.99 £8.99 S$14.99
The Checkout Graph Max Token 9781600000041 $15.99 €14.99 £12.99 S$21.99
Embeddings & Espresso Clara Context 9781600000058 $13.99 €12.99 £10.99 S$18.99
The Retrieval Society Jonas Index 9781600000065 $16.99 €15.99 £13.99 S$22.99
Guardrails for Dreaming Machines Mira Policy 9781600000072 $9.99 €8.99 £7.99 S$13.99
Ten Thousand Tiny Agents Theo Router 9781600000089 $14.99 €13.99 £11.99 S$19.99
The Sandbox Bookshop Samira Stripe 9781600000096 $12.99 €11.99 £10.99 S$17.99
When the Model Learned to Read Lucie Latency 9781600000102 $11.99 €10.99 £9.99 S$15.99

Each ISBN is fictional but carries a valid ISBN-13 check digit, and is added to Stripe Product and Price metadata as isbn. The seed script uses that ISBN to create deterministic Stripe Product IDs in the format nebius_partners_book_<isbn>. For example, The Nebius Cloud Atlas is created as nebius_partners_book_9781600000010.

Each item includes a cover_image_path alongside books.json. Stripe Product images are public URLs rather than local file uploads: if you set STRIPE_IMAGE_BASE_URL, the seed script sends images[0] for each Stripe Product by joining that public base URL with the cover filename. If data/stripe_books.json already exists, rerunning the seed script with STRIPE_IMAGE_BASE_URL set backfills the image URL on each existing Stripe Product without creating new Products or Prices. If an older generated catalog contains random prod_... Product IDs, rerunning the seed script creates or updates the deterministic nebius_partners_book_<isbn> Products and writes a fresh catalog.

The generated catalog lives at data/stripe_books.json. It is gitignored because Stripe Price IDs are account-specific even though the Product IDs are deterministic; the Price IDs are the runtime handles passed to Stripe MCP when creating a Payment Link.

Useful commands:

uv run python scripts/seed_stripe_books.py --dry-run
uv run python scripts/seed_stripe_books.py --force

API

POST /agent/run handles normal book prompts through the guarded Nebius agent. Purchase prompts create a pending approval instead of calling Stripe.

curl -N -X POST http://localhost:8000/agent/run \
  -H 'content-type: application/json' \
  -d '{"thread_id":"checkout-demo","user_id":"reader-42","prompt":"I want to buy The Nebius Cloud Atlas."}'

Relevant SSE events:

event: status
data: {"phase":"input_guardrail","rule":"all","outcome":"passed"}

event: approval_required
data: {"approvalId":"...","action":"stripe.create_payment_link","book":{"title":"The Nebius Cloud Atlas","amount":1499,"currency":"usd"}}

event: answer
data: {"text":"I can create a Stripe test-mode checkout link ... but I need your approval first."}

POST /approvals/{approval_id} approves or rejects the pending side effect:

curl -X POST http://localhost:8000/approvals/$APPROVAL_ID \
  -H 'content-type: application/json' \
  -d '{"decision":"approve"}'

curl -X POST http://localhost:8000/approvals/$APPROVAL_ID \
  -H 'content-type: application/json' \
  -d '{"decision":"reject"}'

Approvals are process-local in this recipe. Move them to Postgres or Redis before using this pattern across replicas.

Design decisions

The approval gate is the core safety mechanism. app/core/approvals.py keeps an in-memory ApprovalStore keyed by a generated approval ID, with a status of pending, completed, rejected, or expired. Reading a record checks its expiry first, so a request against a stale approval is marked expired before any decision is processed. _require_pending refuses to reject or complete anything that is not currently pending, which makes the endpoint idempotent: calling /approvals/{approval_id} again on an already completed or rejected record returns that final state instead of taking a second action. The Stripe MCP call in app/routes/approvals.py only happens inside the approve branch, after that pending check passes.

Use a Stripe Sandbox or test-mode key (sk_test_, rk_test_) and never a live key. Test mode is not enforced in code. app/config.py defaults stripe_mcp_api_key and stripe_secret_key to placeholder strings that look like test keys (sk_test_replace_me, rk_test_replace_me), and the seed script calls the same https://api.stripe.com/v1 endpoint that Stripe uses for both test and live keys. Nothing validates the key prefix at startup or before a call. Safety depends entirely on the operator following the README’s instruction to use a Stripe Sandbox or test-mode account and never a live key.

Failure modes

  • Approval expiry. APPROVAL_TTL_SECONDS (default 900) bounds how long a pending approval stays valid. An expired approval returns HTTP 410 rather than silently falling through to Stripe.
  • Missing Stripe Price ID. StripeMCPClient.create_payment_link raises before calling Stripe if the selected book has no stripe_price_id, which happens if make seed-stripe-books was never run or targeted a different catalog file.
  • Stripe MCP errors. Any HTTP error or JSON-RPC error from https://mcp.stripe.com is wrapped in StripeMCPError and surfaced as HTTP 502, not swallowed.
  • Missing local cover images. The seed script refuses to run if a book’s cover_image_path does not exist on disk next to books.json, unless in --dry-run mode.

Test it

make test
make lint

The test suite covers:

  • guarded normal book answers
  • non-book refusal
  • prompt-injection refusal
  • PII redaction
  • approval creation without Stripe calls
  • rejected approvals
  • approved Stripe MCP calls
  • idempotent completed approvals
  • expired approvals
  • Stripe REST seed script behavior

Tests mock Nebius, Stripe REST, and Stripe MCP with respx, so the default suite is network-free. If you add an optional manual smoke workflow against a live Stripe Sandbox, keep it opt-in and out of pull requests unless the workflow is explicitly marked sandbox-only.

Ship it

A Dockerfile is included and builds a two-stage image (python:3.12-slim builder plus runtime) that runs uvicorn as a non-root app user, with a container healthcheck against /healthz.

make docker
make run-docker

Inherited Prometheus metrics remain available on /metrics. This recipe adds:

  • approval_events_total{status}
  • stripe_mcp_requests_total{tool,outcome}
  • stripe_mcp_duration_seconds{tool}

LangSmith traces record the prompt, thread, user, environment, and model when tracing is enabled. Stripe key values and checkout URLs are not logged by the app.

Going further

  • Persist approvals in Postgres with row-level ownership by authenticated user.
  • Add webhook handling for checkout.session.completed.
  • Use Stripe restricted keys per environment and rotate them from CI.
  • See the rest of the arc in nebius/nebius-partner-cookbook, including the guardrails cookbook this recipe builds on and the Snowglobe testing cookbook that follows it.

Reference