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.
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 recordToolkit.tool()creates: a name, the function, the required capability (orNonefor public), and a description.Caller— who is asking, and what capabilities they hold.ANONYMOUSis 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 (orNone) into aCaller. 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 anIdentityinto a realmcp.server.MCPServer, with per-request capability filtering wired in viaCapabilityMiddleware.asgi_app/run_stdio— the two ways to serve aGate.
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.
Gatemerges 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 sameGateand want their capabilities kept separate even when the verb matches, prefix the domain further (e.g.printer_a:read_statusvs.printer_b:read_status) —mcp-gatedoesn't care what characters you use, since it only ever compares full strings. requires=Nonemarks 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 yourIdentity.
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
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.