mcp-plan-gate-example
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-plan-gate-examplelist my available tools"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-plan-gate-example
Reference implementation: gating a SaaS MCP endpoint by subscription plan — auth, entitlements, metering, and multi-tenant scoping.
This is a small, runnable TypeScript SaaS with a single MCP (Model Context
Protocol) endpoint at POST /mcp. It demonstrates the four things that come
up in almost every "put MCP behind our paid plans" engagement:
Auth -- resolving an MCP caller to an account via a Bearer API key.
Entitlements -- filtering and enforcing which tools a plan can see and call.
Metering -- logging every tool call and enforcing a monthly quota per plan.
Tenant scoping -- making sure every tool only ever touches its caller's own data.
Nothing here is exotic. That is the point: this is meant to be the kind of code you can drop into an existing SaaS's codebase in an afternoon, not a framework.
Architecture
MCP client / agent mcp-plan-gate-example
(Claude Desktop, Cursor, POST /mcp
custom LLM app, curl) ------------> +--------+ +-----------+ +------------------------+
| auth | --> | plan gate | --> | metering / quota gate |
+--------+ +-----------+ +------------------------+
Bearer API src/planGate.ts src/metering.ts
key -> user filters logs usage_events,
src/auth.ts tools/list, enforces monthly quota
| blocks tools/call |
| for non-entitled v
| tools +------------------+
+----------------------------> | tool handlers |
| ping / search / |
| get / export / |
| bulk_update |
+--------+---------+
|
v
+--------------------------+
| tenant-scoped SQLite |
| every query filtered by |
| user_id |
| src/tools/records.ts |
+--------------------------+
^
| webhook updates users.plan column
|
+-----------+------------+
| Stripe (test mode) |
| checkout.session.completed,
| customer.subscription.* |
| src/billing.ts |
+---------------------------+The plain REST side of the SaaS (POST /signup, GET /me, GET /usage) sits
next to /mcp and shares the same users table -- MCP is just another
authenticated surface on an existing API, not a separate system.
Related MCP server: ai-credits-mcp
Why this exists
Teams keep filing real issues and shipping real code for exactly this problem. A few recent precedents:
PostHog shipped a
feature_entitlementgate for MCP tools, resolving an org's plan entitlements at request time and hiding gated tools from unentitled orgs — PostHog/posthog#68838.ErabliereApi merged a
RequirePlanMiddleware-style gate on their/mcpendpoint, restricting the hosted MCP transport to subscription plans that include MCP access — ErabliereApi/ErabliereApi#389.Canny sells MCP connector access as a feature on its paid plans, alongside other Autopilot AI capabilities — Canny pricing.
If your SaaS is adding an MCP endpoint, this is not a hypothetical future problem -- it is the first thing customers, security reviewers, and your own billing team will ask about.
Quickstart
Requires Node 20+.
npm install
npm run seed # creates data/app.db with 3 demo users (free/pro/business)
npm run dev # starts the server on http://localhost:3000npm run seed prints three API keys, one per plan. Save them -- they are
not stored anywhere in plaintext and won't be printed again (a real signup
flow would show the key exactly once, the same way Stripe or GitHub do).
Try the REST side
# Create a new (free-plan) account
curl -s -X POST http://localhost:3000/signup \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com"}'
# Check your account + entitlements
curl -s http://localhost:3000/me -H "Authorization: Bearer <api_key>"
# Check current usage against your monthly quota
curl -s http://localhost:3000/usage -H "Authorization: Bearer <api_key>"Try the MCP endpoint directly
# 1. initialize
curl -s -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer <pro_api_key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}'
# 2. tools/list -- notice the tool list differs by plan
curl -s -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer <pro_api_key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
# 3. tools/call -- a tool this plan is entitled to
curl -s -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer <pro_api_key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_records","arguments":{"query":"a"}}}'
# 4. tools/call -- a tool this plan is NOT entitled to (try with a free-plan key)
curl -s -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer <free_api_key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"search_records","arguments":{"query":"a"}}}'The rejected call returns a normal JSON-RPC error with an upgrade message, not a bare HTTP 403 -- MCP clients (and the LLMs driving them) can surface that message directly to the end user.
Connect it to Claude Desktop or Cursor
Most MCP clients support a mcp-remote-style bridge for HTTP servers, or
native Streamable HTTP config. Example config block (adjust to your
client's config file location):
{
"mcpServers": {
"plan-gate-demo": {
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer <api_key_from_seed_script>"
}
}
}
}With a free-plan key, the client will only ever see ping. Swap in the
pro or business key (from npm run seed) and re-list tools to see the
difference live.
Walkthrough of the four concerns
Concern | Source file |
Auth (Bearer API key -> user) | |
Plan gating (filter | |
Metering + quota enforcement | |
Multi-tenant scoping | |
Wiring it all together per-request | |
Tool definitions | |
Billing (Stripe test mode, optional) |
Auth (src/auth.ts): a Bearer API key resolves to a row in users.
The file has a comment block on exactly where full OAuth 2.1 (per the MCP
authorization spec)
would slot in later -- API keys are the pragmatic v1 almost every SaaS
ships first, because customers already think in terms of API keys from
the REST API.
Plan gating (src/planGate.ts): one declarative map,
PLAN_ENTITLEMENTS, says which tools each plan includes. Two functions
enforce it: filterToolsForPlan narrows what tools/list returns (so
non-entitled tools are invisible, not just blocked), and
assertToolEntitlement is a second, defense-in-depth check inside
tools/call (because clients cache lists, and models don't always respect
what they were told is unavailable).
Metering (src/metering.ts): every tools/call -- success or failure
-- is written to usage_events. assertWithinQuota checks the caller's
plan-specific monthly limit (free = 50, pro = 5,000, business = unlimited)
before the tool runs, and throws a normal MCP error when exceeded.
GET /usage exposes the same numbers to the caller directly.
Tenant scoping (src/tools/records.ts): every single query is filtered
by user_id, including inside bulk_update, where attempting to touch
another tenant's record ID quietly no-ops (updated: false) instead of
throwing -- so a caller probing for other tenants' IDs learns nothing.
Adapting this to your SaaS
Start with ADOPTING.md — the ten questions to answer before putting your own MCP endpoint behind a paid plan, each mapped to the file in this repo that answers it.
This repo is meant to be gutted and rewired, not run as-is in production. The seams are intentionally narrow:
API keys -> your auth. Replace
resolveApiKeyinsrc/auth.tswith a lookup against your existing session/API-key/JWT system. Everything downstream only needs a resolved user object with anidand aplan-- it does not care how you got there.Entitlement map -> your plan/feature-flag source.
PLAN_ENTITLEMENTSinsrc/planGate.tsis deliberately just a plain object. Point it at your existing feature-flag service, plan config table, or entitlements API instead of hardcoding it -- the filtering and blocking logic around it does not change.Usage table -> your billing meter.
usage_eventsinsrc/metering.tsis a stand-in for whatever usage-metering pipeline you already have (a metrics warehouse, Stripe usage records for metered billing, an internal rate-limiter). Swap the read/write calls inmetering.tsfor calls into that system;planGate.tsand the tool handlers never need to know.Stripe wiring is optional and replaceable. In a real engagement, the work is almost never "build billing from scratch" -- it's "point
applyPlanFromStripeEvent(insrc/billing.ts) at the client's existing billing source of truth" so theirusers.plancolumn stays correct.
Testing
npm testCovers:
plan gating filters
tools/listcorrectly for each plan (test/planGate.test.ts)non-entitled
tools/callrequests are rejected with an upgrade message (test/planGate.test.ts)monthly quota enforcement kicks in once a plan's limit is hit (
test/quota.test.ts)tenant scoping: one account can never read or modify another account's records (
test/tenantScoping.test.ts)
Project layout
src/
db.ts SQLite connection + schema (users, records, usage_events)
apiKey.ts demo API key generation
auth.ts Bearer API key -> user resolution
planGate.ts entitlement map + tools/list filtering + tools/call blocking
metering.ts usage logging + monthly quota enforcement
billing.ts Stripe (test mode) plan-sync logic, fully optional
server.ts Express app entrypoint
mcp/server.ts per-request MCP Server construction (tools/list, tools/call handlers)
routes/
core.ts POST /signup, GET /me, GET /usage
mcp.ts POST /mcp (Streamable HTTP transport)
billing.ts POST /billing/checkout, POST /billing/webhook
tools/
definitions.ts tool metadata + handlers (ping, search_records, get_record, export_data, bulk_update)
records.ts tenant-scoped data access
scripts/
seed.ts creates 3 demo users (free/pro/business) + fake records
test/
testServer.ts test harness (real HTTP against an isolated SQLite file)
planGate.test.ts
quota.test.ts
tenantScoping.test.tsLicense
MIT -- see LICENSE.
Built by Praj Vaggu (UV PRAJ TECH INC). I wire MCP endpoints into existing SaaS auth/billing. GitHub @TechCodeCrafter.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/TechCodeCrafter/mcp-plan-gate-example'
If you have feedback or need assistance with the MCP directory API, please join our Discord server