Skip to main content

Table of Contents

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.
You rarely need to hand-roll a 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 the additionalTools 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.
The 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, override getCustomerRef 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 the ctx.respond(data, options?) envelope — never a raw object and never a hand-built content: [{ type: 'text', ... }]. The envelope drives the three SolvaPay response modes:
  • Silentctx.respond(data). The merchant’s data is the hero. No iframe, no upsell. This is the 90% path for a paying customer.
  • Nudgectx.respond(data, { nudge }). Data is returned and something is worth flagging (low balance, cycle ending). The nudge message is appended to content[0].text as 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 on structuredContent. No iframe opens for a gate — the model reads the narration and calls the recovery tool, which mounts the UI.
options carries:
  • text — override content[0].text (use this to give the host a render instruction or a one-line summary instead of the SDK’s JSON.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.
The handler 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 that createSolvaPayMcpServer 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 — topup-first: a customer with credits activates immediately, while a zero-balance customer gets topup_required and is routed through the topup tool. The plan activates as part of the successful top-up.
  • Recurring / hybrid plan — returns a hosted checkoutUrl when the customer has no balance and no card on file, which the agent surfaces as a checkout link.
Plans are managed on the product in the SolvaPay Console. Customers select a plan during activation and the SDK resolves the correct plan from their purchase automatically.

Error Handling

Paywall gate outcomes are not exceptions. The paywall pre-check inside registerPayable 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 mirroring examples/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/fetch so each request is handled without a persistent session store.
  • Text-only hosts — pass hideToolsByAudience: ['ui'] to keep the LLM-facing tools/list narrow to the four intent tools plus your own data tools, while leaving the UI transport tools callable from the SolvaPay iframe. ChatGPT-originated tools/list requests are auto-detected and still receive the full catalog.

Low-level Adapter (escape hatch)

Prefer createSolvaPayMcpServer + 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:
Legacy consumers that still try/catch a PaywallError keep working — it is exported from @solvapay/server as a compat shim:

Next Steps