mcp-gate

mcp-gate

Enables serving multiple MCP toolkits behind one server with capability-based access control, so different callers see and can call only the tools they are authorized for, over stdio or streamable HTTP with bearer-token auth.

Category
访问服务器

README

mcp-gate

mcp-gate lets you mount several MCP toolkits behind one server and filter which tools each caller can see and call — without toolkits knowing about your auth or tier model.

Capability-gated, multi-toolkit MCP tool serving on top of the official mcp SDK.

mcp-gate lets you register tools once, tag each with an optional capability, and serve the same set of tools to different callers who see different subsets of them — a public tool visible to anyone, and a gated tool visible only to a caller holding the right capability. It works over stdio (single local process) and over stateless streamable HTTP (multiple callers, bearer-token auth), with no branching between the two in your tool code.

mcp-gate has no dependency on any specific web framework, ORM, or user model. Its only third-party dependencies are mcp, asgiref, and starlette. A host application (Django, FastAPI, a bare script — anything) supplies its own Identity that resolves a bearer token to a Caller and its capabilities; mcp-gate never touches your database or auth system directly.

Install

pip install git+https://github.com/RunLit/mcp-gate.git

(This package is not yet published to PyPI.)

Core concepts

  • Toolkit — a named, passive registry of tools. You decorate plain functions with @kit.tool(requires=...); the function's docstring becomes the description the model reads.
  • ToolSpec — the record Toolkit.tool() creates: a name, the function, the required capability (or None for public), and a description.
  • Caller — who is asking, and what capabilities they hold. ANONYMOUS is the caller used when no credential was presented, or the credential didn't resolve to anything.
  • Identity — a protocol with one async method, resolve(token) -> Caller, that turns a raw bearer token (or None) into a Caller. This is the one seam a host application implements against its own user store. Two reference implementations ship in the package:
    • StaticTokenIdentity — an in-memory token table, {token: (subject, capabilities)}. Good for a single-owner standalone deployment or tests; anything with real users should resolve against a database instead.
    • OpenIdentity — grants a fixed capability set to every caller, ignoring the token entirely.
  • Gate — assembles one or more toolkits and an Identity into a real mcp.server.MCPServer, with per-request capability filtering wired in via CapabilityMiddleware.
  • asgi_app / run_stdio — the two ways to serve a Gate.

Capability-naming convention

A capability is just a string; mcp-gate does no parsing or validation of it beyond exact-match comparison. Two things follow from that:

  • Capability names are global, not toolkit-scoped. Gate merges tool names from every mounted toolkit (colliding tool names are rejected unless you call .prefixed()), but capability strings get no such treatment — two toolkits that both use "read_status" share that capability whether you meant them to or not.
  • Convention: name capabilities <verb>_<domain>, snake_case, with the domain naming the toolkit or resource the tool touches — e.g. read_printer, control_printer, read_solar. If you mount toolkits from unrelated domains in the same Gate and want their capabilities kept separate even when the verb matches, prefix the domain further (e.g. printer_a:read_status vs. printer_b:read_status) — mcp-gate doesn't care what characters you use, since it only ever compares full strings.
  • requires=None marks a tool public. There is no wildcard capability and no implicit inheritance: a caller who should see everything (a superuser, say) needs every capability name granted explicitly by your Identity.

Worked example

# toolkit.py
from mcp_gate import Toolkit

kit = Toolkit("greeter")

@kit.tool(requires=None)
def greeting() -> str:
    """Say hello. Available to anyone, including anonymous callers."""
    return "hello"

@kit.tool(requires="read_printer")
def printer_status() -> str:
    """Report the printer's current status. Requires the read_printer capability."""
    return "idle"

Serving on stdio, with OpenIdentity

Stdio has no headers and no bearer token — the OS process boundary is the trust boundary. Anyone who can start the process already has whatever access starting it implies, so OpenIdentity (a fixed capability set, no credential check) is the right identity here:

# stdio_main.py
from mcp_gate import Gate, OpenIdentity, run_stdio
from toolkit import kit

gate = Gate(
    name="my-local-server",
    toolkits=[kit],
    identity=OpenIdentity(["read_printer"]),
)

if __name__ == "__main__":
    run_stdio(gate)
python stdio_main.py

Serving over HTTP, with StaticTokenIdentity

# http_main.py
from mcp_gate import Gate, StaticTokenIdentity, asgi_app
from toolkit import kit

identity = StaticTokenIdentity({
    "super-secret-token-for-rnl": ("rnl", ["read_printer"]),
})

gate = Gate(name="my-http-server", toolkits=[kit], identity=identity)
app = asgi_app(gate)  # a plain ASGI app
uvicorn http_main:app --host 0.0.0.0 --port 8000

A request with Authorization: Bearer super-secret-token-for-rnl sees both greeting and printer_status in tools/list, and can call either. A request with no token, or an unrecognized one, resolves to ANONYMOUS and sees only greeting; calling printer_status anyway raises the same "tool not found"-shaped error as calling a tool that doesn't exist at all — gating never leaks which restricted tools exist to a caller who can't use them.

asgi_app(gate, **streamable_http_kwargs) passes any extra keyword arguments straight through to the SDK's streamable_http_app() (host, transport_security, json_response, streamable_http_path, max_request_body_size). This matters once you're behind nginx (or similar) fronting a real domain: streamable_http_app() auto-enables a DNS-rebinding guard that only allows Host headers matching 127.0.0.1/localhost/::1, so a request for your production domain gets a 421 Misdirected Request unless you pass a transport_security (or host) that allows it, e.g.:

from mcp.server.transport_security import TransportSecuritySettings

app = asgi_app(
    gate,
    transport_security=TransportSecuritySettings(allowed_hosts=["mcp.example.com"]),
)

Warning: never use OpenIdentity over HTTP

OpenIdentity grants its fixed capability set to every caller and never looks at the token — it exists for stdio, where the process boundary already is the trust boundary. Wiring OpenIdentity into asgi_app() means every HTTP request, from anyone who can reach the port, gets that full capability set with no credential check whatsoever. Use StaticTokenIdentity or your own Identity (backed by real per-user credentials) for anything served over asgi_app().

Running the tests

pip install -e ".[dev]"
pytest tests/ -v

推荐服务器

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 模型以安全和受控的方式获取实时的网络信息。

官方
精选