@inception-agents/expansion-pack
v0.8.1
Published
Vendor-side expansion pack for MCP servers — turn plan-limit blocks into an agent-completable upgrade loop (your entitlements, your Stripe, Inception's invariant + intelligence)
Readme
@inception-agents/expansion-pack
Drop-in expansion pack for your MCP server: turn plan-limit blocks into an agent-completable upgrade loop. Your entitlement logic stays local and authoritative; your Stripe (connected to Inception via Stripe Connect) powers options + checkout; Inception holds the commerce invariant, wall provenance, and lifecycle intelligence server-side.
On MCP Apps-capable hosts (Claude, ChatGPT), the upgrade loop also renders branded in-chat cards — plan comparison, path chooser, trial countdown, admin approval, value receipt — with your logo, accent color, and copy, served straight from your MCP server. No extra wiring: the same registerExpansionPack() call registers them, and every card degrades to the plain-text flow on hosts without MCP Apps.
Installation
npm install @inception-agents/expansion-pack @modelcontextprotocol/sdk zod@modelcontextprotocol/sdk and zod are peer dependencies — you already have both if you run an MCP server.
Quick Start
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { registerExpansionPack } from '@inception-agents/expansion-pack';
const server = new McpServer({ name: 'my-saas', version: '1.0.0' });
registerExpansionPack(server, {
apiKey: process.env.IA_API_KEY!, // Inception tenant API key (sk_...)
resolveEntitlement: async ({ action, subjectRef }) => {
// YOUR entitlement logic — runs locally, stays authoritative.
const ok = await myPlanService.can(subjectRef, action);
if (ok) return { allowed: true };
return {
allowed: false,
reason: {
code: 'seat_limit',
message: 'Workspace is at its 5-seat cap.',
limit: 5,
current: 5,
wallKey: 'plan.seats',
},
};
},
// Optional: pseudonymous stable end-user ref derived from YOUR MCP
// session context. Hash your user/workspace id — NEVER raw PII.
getSubjectRef: (extra) => hashWorkspaceId(extra),
// Optional: where Stripe Checkout returns the buyer.
returnUrl: 'https://my-saas.app/billing',
});The four tools
| Tool | What it does |
| --- | --- |
| check_entitlement | Runs your local hook. Blocked → reports the verdict to Inception (which mints the wall event) and returns a trigger_context. |
| get_upgrade_options | Lists YOUR Stripe plans that unblock the action. Only works with a live trigger_context. |
| initiate_upgrade | Creates a Stripe Checkout session on your connected account and returns the URL for the human to complete. |
| confirm_entitlement_update | Re-runs your local hook; when it now passes, closes the loop (upgrade_accepted + entitlement_resumed) and tells the agent to resume the original task. |
In-chat cards (MCP Apps)
When your McpServer supports the modern registration surface (registerTool/registerResource — @modelcontextprotocol/sdk ≥ 1.13), the pack additionally:
- registers the card widgets as
ui://inception/{template}@{version}resources (SEP-1865 MCP Apps, mimetext/html;profile=mcp-app); - points the card-bearing tools at them via tool-def
_meta(emitted in both the nestedui.resourceUriand flatui/resourceUridialects for maximum host compatibility); - attaches
structuredContentto results so the rendered card gets live data (plans, trial deadline, receipt); - registers the app-only
create_payment_sessiontool (hidden from the model; callable only by the card widget) for sovereign in-card Stripe checkout; - registers the app-only
refresh_upgrade_quote+report_card_interactiontools (0.6.0) that power the card's in-card configuration loop and interaction telemetry.
Call registerExpansionPack BEFORE server.connect() — some hosts reject resource/capability registration after the transport connects (the pack then degrades to text-only tools; it never crashes).
Branding is server-controlled. The card pulls your PUBLISHED branding config (logo, accent, copy, offer levers) from Inception's API with your sk_ key, cached 5 minutes — marketers change the live card from the Inception dashboard/admin tools with no redeploy of your server.
Full card theming (0.5.0, Growth plan+). Beyond the accent: colorScheme can commit the card to your brand's light or dark surfaces (cardBg/cardSurface/cardBorder/textPrimary/textSecondary), cornerStyle: "pill" fully rounds the buttons, and your logo renders from a validated data URI. Theming is delivered agent-side — connect the Inception MCP and ask your assistant to extract your brand from your site (or your Stripe checkout branding) and publish it; WCAG contrast floors are enforced server-side. The trust chrome (lock + "Secure checkout via Stripe", the price on the CTA, the decline option) recolors with your theme but always renders. SDKs older than 0.5.0 silently degrade to accent-only theming.
Interactive cards (0.6.0, every plan). The card is now a size ladder — compact answers "what do I do?" (the wall's own words, the recommended plan, the price on the button), expanding answers "is this right for me?" (the buyer's own usage numbers, current→target plan delta, grounded fit reasons with honest tradeoffs), and fullscreen (host-permitting) is the full plan matrix in-thread. Buyers can configure the purchase inside the card: a seat/pack quantity stepper (interactive.quantity + routing.seatSelection), a monthly/annual toggle, and tappable plan selection — every change re-quotes server-side and the button re-labels from the signed quote, so the exact confirmed amount is always the exact charged amount. A legal: {termsUrl, privacyUrl} config renders a fixed "Terms · Privacy" row in the trust chrome. Interactivity is never plan-gated — only visual theming is Growth+. The agent is briefed from the same data the card renders (the account briefing in the tool text plus live model-context updates), so "is annual worth it for us?" gets answered from verified numbers, not guesses.
Checkout rails per host. The card's checkout is open_link (hosted Stripe Checkout in a new tab) unless the Inception operator enables sovereign (in-card Stripe Payment Element) for a specific agent environment. This flag is fail-closed and never vendor-set: sovereign only makes sense on hosts that honor CSP frameDomains (ChatGPT/Codex today; Claude pending Anthropic's security review — it degrades to the open-link CTA there automatically).
| Host | Card rendering | Checkout rail | | --- | --- | --- | | ChatGPT (dev-mode connectors) | interactive | sovereign (when operator-enabled) or open_link | | Claude (Desktop / web) | interactive/display | open_link | | Hosts without MCP Apps | — (text flow) | checkout URL in text |
Version ladder. Cards need @modelcontextprotocol/sdk ≥ 1.13 (registerTool/registerResource); tool-def _meta requires ≥ ~1.16 — on 1.13–1.15 cards still render from result-level _meta. On anything older (or with enableCards: false) you get exactly the classic text-only registration.
In-thread execution (elicitation + execute_upgrade)
On hosts that declare the MCP elicitation capability (Claude Code CLI), a
wall with a saved payment method completes in-thread: initiate_upgrade
quotes the exact amount due today, asks the user natively (our copy, their
card's brand + last4, a single confirm boolean), and executes server-side on
accept. Other hosts get the checkout link plus a quote block the agent may
act on via execute_upgrade — only after the user explicitly confirms the
exact amount. Every miss degrades byte-identically to the checkout-URL flow,
and nothing is ever charged without the amount echo matching the quote.
Default-ON; kill per tenant/host with set_payment_tier
{"default":"links_only"}. Details: docs/in-thread-upgrade-execution.md.
Presentation priority (ui_mode)
The choice moment is native-first by default: on Claude Code and Claude
Desktop the plan-comparison card is suppressed (and on elicitation-capable
hosts the choice itself is a native server-driven form) so the host's own
question UI owns the moment; ChatGPT keeps the card. Operators reorder the
layers per environment with admin-mcp set_ui_mode — no redeploy. Details:
docs/in-thread-upgrade-execution.md.
Guarantees
- The invariant is server-side. No upgrade offer, checkout, or confirmation is ever served without a live wall event minted by a preceding blocked check. Triggers expire after 1 hour and are single-use.
- Fail-open. If Inception's API is unreachable (5s timeout), your tools still answer from your local verdict — blocked stays blocked (without upsell), allowed stays allowed. Your MCP server never breaks because of us. (One deliberate exception:
create_payment_sessionfails CLOSED — a payment session is never fabricated; the card falls back to the open-link CTA.) - No PII.
subjectRefmust be pseudonymous (1-128 chars of[A-Za-z0-9._:-]); the shape is enforced server-side. Emails, names, and phone numbers are rejected by the charset. - Zero runtime dependencies beyond the peer deps you already have. The card widgets and wire helpers are compiled into this package (~400 KB installed, nothing fetched at runtime except your own branding config); payment credentials (
clientSecret) ride only instructuredContent, never in model-visible text.
Prerequisites
- An Inception Agents account with a tenant API key (
sk_...). - Your Stripe account connected via the dashboard (Stripe Connect) — this is what powers
get_upgrade_options/initiate_upgradewith YOUR plan catalog and YOUR checkout.
Configuration
| Option | Required | Description |
| --- | --- | --- |
| apiKey | yes | Inception tenant API key. |
| resolveEntitlement | yes | Your local entitlement hook: ({action, subjectRef}) => {allowed: true} \| {allowed: false, reason}. |
| apiUrl | no | Defaults to https://api.inceptionagents.com. |
| getSubjectRef | no | Derive a pseudonymous end-user ref from your MCP tool extra context. Never raw PII. |
| getAgentEnvironment | no | Return the RAW MCP client name for the session; the SDK normalizes it (claude_code / claude_desktop / chatgpt / cursor / generic) and sends agent_environment on every call, so responses come back with per-environment presentation guidance (numbered options, bare vs markdown links) and wall analytics slice by environment. Also selects the per-environment checkout rail for cards. |
| returnUrl | no | Post-checkout return URL (gets ?upgrade=success\|canceled appended). |
| enableCards | no | MCP Apps in-chat cards. Default true (feature-detected — text-only on servers without registerTool/registerResource). Set false to force text-only everywhere. |
| uiBrandingTtlMs | no | Branding/payment-tier config cache TTL. Default 5 minutes; refresh failures serve the last-known-good config. |
Wiring getAgentEnvironment
With @modelcontextprotocol/sdk's McpServer, the client name is captured at
the initialize handshake and exposed via getClientVersion():
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
registerExpansionPack(server, {
apiKey: process.env.INCEPTION_API_KEY!,
resolveEntitlement: myEntitlements,
// Evaluated lazily on every tool call — safe to wire before connect().
getAgentEnvironment: () => server.server.getClientVersion()?.name,
});