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.Documentation Index
Fetch the complete documentation index at: https://docs.solvapay.com/llms.txt
Use this file to discover all available pages before exploring further.
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/reactand@modelcontextprotocol/ext-appsinstalled in your MCP App bundle
Install
Quick start
Wire the adapter intoSolvaPayProvider 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. ExportMCP_TOOL_NAMES in your server so the two stay in lockstep.
| Transport method | MCP tool name | Returns |
|---|---|---|
checkPurchase | check_purchase | { customerRef, email, name, purchases: [...] } |
createCheckoutSession | create_checkout_session | { sessionId, checkoutUrl } |
createCustomerSession | create_customer_session | { sessionId, customerUrl } |
getPaymentMethod | get_payment_method | { kind: 'card', brand, last4, expMonth, expYear } | { kind: 'none' } |
getMerchant | get_merchant | Merchant |
getProduct | get_product | Product |
listPlans | list_plans | Plan[] |
getBalance | get_customer_balance | { credits, displayCurrency, creditsPerMinorUnit, displayExchangeRate } |
createPayment | create_payment_intent | PaymentIntentResult |
processPayment | process_payment | ProcessPaymentResult |
createTopupPayment | create_topup_payment_intent | TopupPaymentResult |
activatePlan | activate_plan | ActivatePlanResult |
cancelRenewal | cancel_renewal | CancelResult |
reactivateRenewal | reactivate_renewal | ReactivateResult |
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.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’scustomer_ref, the provider only needs a sentinel token to flip isAuthenticated true. Supply a lightweight auth adapter alongside the transport:
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.
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, Update card and Cancel plan actions. The card returns null when there is no active purchase, so you can render it unconditionally.
<CurrentPlanCard />renders the active plan, mirrored card brand/last4, and inline Update card / Cancel plan actions.<LaunchCustomerPortalButton />opens the hosted customer portal in a new tab. It pre-fetchescreateCustomerSessionon hover so the portal link is ready the moment the user clicks (anchor-click semantics are preserved for sandboxed hosts).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 thepayment_intent.succeededwebhook 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.payable.mcp 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 atexamples/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.
ChatGPT host caveats
ChatGPT’s Custom Connector is the most permissive MCP host for SolvaPay’s iframe surface, but two of its behaviours are easy to trip over.Iframe tools/call must appear in tools/list
ChatGPT’s gateway re-validates every iframe-initiated tools/call against the connector’s cached tools/list catalog. If a tool isn’t in the catalog, the gateway returns MCP error -32000: MCP Resource not found without forwarding the request to your server. The iframe sees the error as Top-up initialization failed (or the equivalent message for the active flow) and the worker tail shows nothing.
This collides with hideToolsByAudience: ['ui'] — the SDK option that hides the seven UI transport tools (create_payment_intent, create_topup_payment_intent, process_payment, create_checkout_session, create_customer_session, cancel_renewal, reactivate_renewal) from tools/list so the model only sees the four intent tools (upgrade, manage_account, activate_plan, topup). On every other host the iframe can still invoke the hidden tools because the host proxies postMessage straight through; on ChatGPT it can’t.
The SDK handles this for you: when tools/list is requested by ChatGPT (detected via request.headers['user-agent'] matching /openai-mcp/i, with a clientInfo.name fallback), the audience filter is bypassed and the full eleven-tool catalog is returned. Every other host gets the trimmed catalog. Use the option exactly as you’d expect:
console.warn per server instance when it fires, so it’s visible in your tail without flooding it.
tools/list is cached per (organization, connector)
ChatGPT fetches tools/list once when an organization adds the connector and reuses the cached catalog indefinitely. Disconnecting your personal OAuth and reconnecting does not invalidate the cache — neither does redeploying your server. If you change the tool surface (add a tool, rename a tool, change a description), an org with the connector already added will keep using the stale catalog.
To force a refresh:
- Delete the connector entirely (Settings → Connectors → connector → Delete, not “Disconnect”), then re-add it from the same URL. This is the only reliable invalidation.
- Or test the change from a different ChatGPT workspace / organization that hasn’t seen the connector before.
Known boundaries
trackUsagestays on the server. Usage metering belongs on your backend, not the client — continue to callsolvaPay.trackUsage(...)from@solvapay/serverinside your tool handlers.
Next steps
- MCP Server integration — server-side paywall patterns with
payable.mcp() - React SDK guide — hooks and components used under
createMcpAppAdapter - Purchase management — cancel, reactivate, renewal semantics