Table of Contents
- Installation
- Basic Setup
- Protecting Tools
- Authentication
- Response Format
- Plan Activation
- Error Handling
- Complete Example
- Low-level Adapter (escape hatch)
Installation
Install the SDK packages plus the official MCP SDK and Zod:@solvapay/mcp is the only package that imports @modelcontextprotocol/*. The framework-neutral contracts (bearer helpers, paywall envelope) live in @solvapay/mcp-core, which is installed transitively.
Basic Setup
1. Initialize SolvaPay
2. Create the MCP server
createSolvaPayMcpServer returns a fully wired McpServer. It auto-registers the transport and intent tools (upgrade, manage_account, topup, activate_plan), the UI resource referenced by those tools, the slash-command prompts, the narrated docs://solvapay/overview.md resource, and the Stripe CSP baseline. You only supply your product and where the UI bundle lives.
new McpServer(), ListToolsRequestSchema handler, or JSON-Schema Tool[] array — the factory handles all of it. If you genuinely need that level of control, see the low-level adapter.
Protecting Tools
Register your paywall-protected tools inside theadditionalTools hook. Each registerPayable call takes a Zod schema and a handler that returns the response envelope via ctx.respond(...). The SDK runs the paywall pre-check before your handler — if the customer is out of credits or needs to upgrade, your handler never runs and SolvaPay returns the gate automatically.
schema shape flows through to the handler’s args, so query is typed as string without a second declaration. Register as many tools as you like inside the same hook — they all protect against the server’s productRef unless you pass a per-tool product.
Annotations are required
Every tool advertises MCP annotations.registerPayable defaults to { readOnlyHint: true, openWorldHint: true } — the right shape for a paywalled data tool that reads from your backend. Override for state-mutating tools:
Authentication
The recommended path is the OAuth bridge. It serves the RFC 9728 discovery endpoints, proxies the OAuth flow to SolvaPay, and validates the bearer token on/mcp. Once mounted, the SDK reads the customer reference from extra.authInfo.extra.customer_ref automatically — your handlers never parse tokens or read a customer_ref argument.
Node / Express
Edge / fetch runtimes
For Deno, Supabase Edge, Cloudflare Workers, Bun, or Next.js edge, use the turnkey fetch factory from@solvapay/mcp/fetch. It bundles the OAuth bridge, transport, and server into one (Request) => Response handler:
Advanced: custom customer-ref extraction
If your server validates tokens itself, overridegetCustomerRef on a tool and decode claims with the bearer helpers from @solvapay/mcp-core. The validated token is available on extra.authInfo.token. These helpers only decode claims (customerRef, customer_ref, sub) — they do not verify signatures, so call them after token validation (for example against /v1/customer/auth/userinfo). Fail closed; never substitute a fallback identity such as anonymous.
Response Format
Handlers return thectx.respond(data, options?) envelope — never a raw object and never a hand-built content: [{ type: 'text', ... }]. The envelope drives the three SolvaPay response modes:
- Silent —
ctx.respond(data). The merchant’s data is the hero. No iframe, no upsell. This is the 90% path for a paying customer. - Nudge —
ctx.respond(data, { nudge }). Data is returned and something is worth flagging (low balance, cycle ending). The nudge message is appended tocontent[0].textas a plain-text suffix that names the recovery intent tool. Never blocks. - Gate — fired automatically by the paywall pre-check when the customer can’t be served. The transport emits a text-only narration naming the recovery intent tool (
upgrade/topup/activate_plan) with the machine-readable gate onstructuredContent. No iframe opens for a gate — the model reads the narration and calls the recovery tool, which mounts the UI.
options carries:
text— overridecontent[0].text(use this to give the host a render instruction or a one-line summary instead of the SDK’sJSON.stringify(data)default).nudge— the inline upsell suffix shown above.units— reserved for V1.1 variable billing. V1 accepts the field for forward-compatible code but bills a fixed one unit per call.
ctx also exposes ctx.customer (balance, remaining, withinLimits, plan, and .fresh() for a non-cached round-trip) and ctx.product so you can make your own nudge decisions without building any UI.
Plan Activation
Plan activation and upgrades are handled by the built-in intent tools thatcreateSolvaPayMcpServer registers — you rarely implement them by hand. activate_plan, upgrade, and topup already map to the underlying flows:
- Free plan — activates immediately; the customer can start calling paid tools.
- Usage-based plan — activates immediately even at a zero balance; top-up is optional and flows through the
topuptool. - Recurring / hybrid plan — returns a hosted
checkoutUrlwhen the customer has no balance and no card on file, which the agent surfaces as a checkout link.
Error Handling
Paywall gate outcomes are not exceptions. The paywall pre-check insideregisterPayable resolves a gate into a normal tool result (isError: false, a narration in content[0].text, and the gate on structuredContent) before your handler runs — there is nothing to try/catch for the happy path. Anything your handler throws (other than an explicit ctx.gate(...)) surfaces as a genuine tool-level error.
To trigger a paywall yourself mid-handler — rare, since the pre-check normally fires first — call ctx.gate(reason?). Handler execution stops and the adapter routes the gate through the same text-only channel:
Complete Example
A compact, copy-paste-safe Express server mirroringexamples/mcp-checkout-app: the OAuth bridge, one registerPayable tool, and the streamable HTTP transport.
Stateless and text-only deployments
- Stateless edge runtimes — use
createSolvaPayMcpFetch({ ..., mode: 'json-stateless' })from@solvapay/mcp/fetchso each request is handled without a persistent session store. - Text-only hosts — pass
hideToolsByAudience: ['ui']to keep the LLM-facingtools/listnarrow to the four intent tools plus your own data tools, while leaving the UI transport tools callable from the SolvaPay iframe. ChatGPT-originatedtools/listrequests are auto-detected and still receive the full catalog.
Low-level Adapter (escape hatch)
PrefercreateSolvaPayMcpServer + registerPayable. Reach for the low-level adapter only when you maintain your own McpServer wiring and can’t adopt the factory.
solvaPay.payable({ product }).mcp(fn) wraps a single business-logic function with the paywall and returns an MCP tool result. When you want full control over the gate response shape, call solvaPay.paywall.decide(...) and format the gate with paywallToolResult from @solvapay/mcp-core:
try/catch a PaywallError keep working — it is exported from @solvapay/server as a compat shim:
Next Steps
- MCP App integration - Render a custom checkout/account UI inside the host iframe
- Express.js Integration Guide - HTTP framework integration patterns
- Usage Events - Track and bill tool usage
- Custom Authentication Adapters - Build custom auth adapters
- API Reference - Full API documentation