Skip to main content
An MCP App is a UI resource that an MCP host loads inside a sandboxed iframe. Host sandboxes typically block direct HTTP calls to arbitrary backends, so the UI cannot hit your API the way a normal React app would. The TypeScript SDK ships a dedicated adapter for this environment. createMcpAppAdapter returns a SolvaPayTransport that tunnels every data call through app.callServerTool instead of HTTP. Mount it on SolvaPayProvider and every hook (usePurchase, useMerchant, <CurrentPlanCard>, <LaunchCustomerPortalButton>, etc.) works unchanged.

Prerequisites

  • An MCP host such as basic-host
  • A SolvaPay product with at least one active plan
  • An MCP server that implements the SolvaPay tool surface — see MCP Server integration for the server-side paywall patterns
  • @solvapay/react and @modelcontextprotocol/ext-apps installed in your MCP App bundle

Install

Quick start

Wire the adapter into SolvaPayProvider and you’re done — every SDK hook routes through the MCP transport.
app.connect() still has to run once before the provider mounts — do it in a top-level bootstrap effect alongside whatever host-context handling you need.

Tool contract

The adapter maps each transport method to a single MCP tool name. Export MCP_TOOL_NAMES in your server so the two stay in lockstep. Your server only needs to implement the tools the UI actually uses. Unimplemented tools surface as a thrown error from the adapter — catch and feature-detect in your component.

Server side

On the server, register each tool with the canonical name so the client adapter can find it. Import the constants so you never hand-type a string.
Pair this with createMcpOAuthBridge from @solvapay/mcp/fetch (or /express) to surface customer_ref on extra.authInfo — the core helpers read it from the synthesised request headers. For the full batteries-included setup use createSolvaPayMcpServer from @solvapay/mcp. A complete working server lives at examples/mcp-checkout-app/src/server.ts.

Authentication

Because the real identity lives server-side on the OAuth bridge’s customer_ref, the provider only needs a sentinel token to flip isAuthenticated true. Supply a lightweight auth adapter alongside the transport:
Without this, the provider short-circuits the fetch pipeline and the transport never runs.

Hosted checkout from inside the iframe

Open checkout in a new browser tab. Pre-fetch the session URL on mount and render a real <a target="_blank"> anchor — scripted window.open after an async round-trip is blocked by typical host sandboxes, but anchor clicks are permitted.
On focus / visibilitychange, call refetch() from usePurchase so returning from the hosted tab flips the card to its new state automatically.

Account management

Once a customer has paid, drop <CurrentPlanCard /> into the tree and the SDK does the rest — plan name, next-billing line, payment-method summary, plus Update card and Cancel plan actions. The card returns null when there is no active purchase, so you can render it unconditionally. The MCP manage_account view passes hideUpdatePaymentButton and hideCancelButton and pairs the card with a single Manage account customer-portal CTA — both flows run through the portal there.
  • <CurrentPlanCard /> renders the active plan, mirrored card brand/last4, and inline Update card / Cancel plan actions. The MCP manage_account view hides both — card updates and cancellation route through the Manage account customer-portal CTA — and surfaces a one-line hint pointing at it.
  • <LaunchCustomerPortalButton /> opens the hosted customer portal in a new tab (default label: “Manage account”). The button renders enabled from first paint and fetches the portal URL in the background; multiple instances under the same provider share a single in-flight createCustomerSession round-trip. When the URL has resolved, click is a real <a target="_blank"> navigation (sandbox-safe). On a cache-miss click the handler awaits the in-flight promise and falls back to window.open.
  • usePaymentMethod() exposes the mirrored card under { paymentMethod, loading, refetch } when you need to build a custom account view. The card brand and last4 come from the payment_intent.succeeded webhook persisted on the Customer — no card-element iframe required inside the MCP App sandbox.

Text-only paywall

The MCP App surface uses SolvaPay’s text-only paywall. A registerPayable tool emits a plain-text Purchase required response — no embedded UI meta, no structured checkout payload — so the host model can read the copy, call create_checkout_session, and surface the returned URL however it likes. There is no McpPaywallView / McpNudgeView / McpUpsellStrip component anymore; render checkout through <PaymentForm> or the hosted URL instead.

Complete example

A full working example — server, client, OAuth bridge, polling, and the five-state purchase flow — lives in the SDK repo at examples/mcp-checkout-app. Clone it, set SOLVAPAY_SECRET_KEY and SOLVAPAY_PRODUCT_REF, point basic-host at http://localhost:3006/mcp, and you have an end-to-end paywalled MCP App running locally.

Known boundaries

  • trackUsage stays on the server. Usage metering belongs on your backend, not the client — continue to call solvaPay.trackUsage(...) from @solvapay/server inside your tool handlers.
  • Inline card entry on Claude. Claude’s MCP Apps host applies a fixed sandbox Content Security Policy that does not forward the spec’s frameDomains declaration. Because Stripe’s card surfaces load in a nested iframe, the inline Payment Element cannot render inside Claude’s widget. The SDK detects this at runtime and falls back to SolvaPay-hosted checkout in a new tab (see Hosted checkout from inside the iframe), so checkout still completes and the payment stays on SolvaPay’s rail. On hosts that honor the spec (e.g. ChatGPT), the inline Payment Element renders directly — no configuration needed.

Next steps