mcp-plan-gate-example

mcp-plan-gate-example

Reference implementation for adding subscription-based gating to MCP endpoints, covering auth, plan entitlements, usage metering, and multi-tenant scoping.

Category
访问服务器

README

mcp-plan-gate-example

CI

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:

  1. Auth -- resolving an MCP caller to an account via a Bearer API key.
  2. Entitlements -- filtering and enforcing which tools a plan can see and call.
  3. Metering -- logging every tool call and enforcing a monthly quota per plan.
  4. 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.

Why this exists

Teams keep filing real issues and shipping real code for exactly this problem. A few recent precedents:

  • PostHog shipped a feature_entitlement gate 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 /mcp endpoint, 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:3000

npm 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) src/auth.ts
Plan gating (filter tools/list, block tools/call) src/planGate.ts
Metering + quota enforcement src/metering.ts
Multi-tenant scoping src/tools/records.ts
Wiring it all together per-request src/mcp/server.ts, src/routes/mcp.ts
Tool definitions src/tools/definitions.ts
Billing (Stripe test mode, optional) src/billing.ts, src/routes/billing.ts

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 resolveApiKey in src/auth.ts with a lookup against your existing session/API-key/JWT system. Everything downstream only needs a resolved user object with an id and a plan -- it does not care how you got there.
  • Entitlement map -> your plan/feature-flag source. PLAN_ENTITLEMENTS in src/planGate.ts is 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_events in src/metering.ts is 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 in metering.ts for calls into that system; planGate.ts and 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 (in src/billing.ts) at the client's existing billing source of truth" so their users.plan column stays correct.

Testing

npm test

Covers:

  • plan gating filters tools/list correctly for each plan (test/planGate.test.ts)
  • non-entitled tools/call requests 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.ts

License

MIT -- see LICENSE.


Built by Praj Vaggu (UV PRAJ TECH INC). I wire MCP endpoints into existing SaaS auth/billing. GitHub @TechCodeCrafter.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选