mcp-gateway
Provides access to GitHub through the MCP Gateway, with tools for operations such as creating issues and merging pull requests, subject to configurable allow/deny policies.
Provides access to Linear through the MCP Gateway, with tools for listing and managing Linear resources, subject to configurable allow/deny policies.
Provides access to Slack through the MCP Gateway, exposing Slack tools via curated profiles with policy controls, rate limits, and audit logging.
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-gatewayshow me the audit log for denied tool calls in the last hour"
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.
Every MCP client keeps its own list of servers, its own copy of your credentials, and its own spawned subprocesses. Open three clients and you have three GitHub servers running, three copies of the same config to maintain, and 120 tools competing for space in the model's context — with no record of which one ran, or with what arguments.
MCP Gateway is a single local daemon that sits in front of all of them. It owns the backend
connections once and exposes curated subsets of their tools as named profiles. Clients
connect to http://127.0.0.1:8420/mcp/<profile> instead of spawning anything.
github ──┐
linear ──┼──► MCP Gateway ──► Claude Code · Claude Desktop · Cursor
fs ──────┤ 127.0.0.1:8420
slack ───┘ policy · limits · auditWhy
One config, not four. Add a server once. Every client sees it.
One process per backend. Three clients connected still means one GitHub subprocess.
Fewer, better tools. Expose 12 relevant tools instead of 120, renamed to whatever reads clearly to the model.
Nothing runs unlogged. Every call — allowed, denied, or failed — is one line of JSON.
Real brakes. A denied tool is invisible and uncallable. Rate limits are per profile.
Tools can't change under you. Descriptions are hashed and pinned; a server that quietly rewrites one gets blocked until you review the diff.
Related MCP server: Miftah
Requirements
Node 20.11 or newer. Nothing else — no database, no Redis, no external services. Three runtime
dependencies: the MCP SDK, yaml, and zod.
Install
Not published to npm. Build it from the repository:
npm install && npm run buildThat produces dist/src/cli.js (mcpgw) and dist/src/bridge.js (mcpgw-bridge). Link them
onto your PATH with npm link if you want the bare command names.
Configure
Create config.yaml. Declare your servers once, then compose profiles from them:
version: 1
listen:
host: 127.0.0.1
port: 8420
servers:
github:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: ${GITHUB_TOKEN}
fs:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "~/code"]
linear:
transport: http
url: https://mcp.linear.app/mcp
headers:
Authorization: Bearer ${LINEAR_KEY}
profiles:
default:
servers: ["*"]
readonly:
servers: [github, linear]
allow: ["github__get_*", "github__list_*", "linear__list_*"]
coding:
servers: [github, fs]
deny: ["github__delete_*", "github__merge_pull_request"]
rename:
github__create_issue: file_bug
limits: { rpm: 60, concurrent: 4 }Credentials come from environment variables via ${VAR} — they never live in the config file.
Then start it:
mcpgw startConnect your clients
Give each client the profile that fits it — coding for your editor, readonly for anything
you trust less. Every entry below replaces that client's direct server entries; nothing but
the gateway should be spawning backends any more.
Claude Code (~/.claude.json) speaks HTTP, so it points straight at a profile:
{ "mcpServers": { "gateway": { "type": "http", "url": "http://127.0.0.1:8420/mcp/coding" } } }Claude Desktop (%APPDATA%/Claude/claude_desktop_config.json, or
~/Library/Application Support/Claude/ on macOS) speaks only stdio, so it launches the bridge —
a shim that pipes stdin/stdout to the daemon and spawns no backends of its own:
{
"mcpServers": {
"gateway": {
"command": "node",
"args": ["/abs/path/to/mcp-gateway/dist/src/bridge.js",
"--url", "http://127.0.0.1:8420/mcp/readonly"]
}
}
}Cursor (~/.cursor/mcp.json) uses the same bridge, usually on a different profile:
{
"mcpServers": {
"gateway": {
"command": "node",
"args": ["/abs/path/to/mcp-gateway/dist/src/bridge.js",
"--url", "http://127.0.0.1:8420/mcp/coding"]
}
}
}If the daemon is not running, the bridge exits immediately with a readable message instead of hanging on the first tool call.
CLI
Command | What it does |
| Run the daemon |
| Check the config without starting; exits non-zero on any error |
| Backend health, uptime, restart counts, active sessions, pending drift |
| Every tool the profile exposes, plus the rule behind each decision |
| Show changed tool descriptions as diffs; |
| Authorize an |
| Re-read the config in the running daemon (what SIGHUP does) |
| Stream the audit log |
mcpgw list is the one to reach for when a tool isn't showing up — it prints the decision and
the exact rule that produced it.
SIGHUP reloads the config, restarting only the servers whose definitions actually changed —
live sessions keep working, and a new allow list applies to them without a reconnect. A bad edit
is rejected and the previous config keeps serving. SIGTERM/SIGINT stop accepting new work,
give in-flight calls up to 5 seconds to finish, then shut the backends down.
Remote servers that need OAuth
A remote MCP server that answers 401 with a WWW-Authenticate header wants an OAuth token,
not a static header. Mark it and authorize it once:
servers:
figma:
transport: http
url: https://mcp.figma.com/mcp
auth: oauth
scope: "mcp:connect"mcpgw auth figmaThat opens a browser, completes the authorization-code flow with PKCE, and writes the tokens to
tools.lock.json's neighbour tokens.json (mode 0600, gitignored). From then on the daemon
connects on its own and refreshes the access token silently; you only run mcpgw auth again if
the refresh token is revoked.
The daemon never opens a browser by itself. A backend that needs authorizing stays DOWN with
needs authorization: run mcpgw auth <server> and does not retry — retrying an expired
authorization only burns backoff until a human acts.
Servers that refuse dynamic registration. Many commercial servers advertise a registration
endpoint and then reject it, because they expect an OAuth app you created by hand. Figma is one.
Create the app with redirect URI http://127.0.0.1:8419/callback, then:
servers:
figma:
transport: http
url: https://mcp.figma.com/mcp
auth: oauth
scope: "mcp:connect"
client_id: ${FIGMA_CLIENT_ID}
client_secret: ${FIGMA_CLIENT_SECRET}The credentials come from the environment like every other secret. With client_id set, no
registration is attempted at all.
Policy
A profile picks servers, then filters their tools with globs. Deny always beats allow, and
if an allow list is present, anything not matching it is excluded. Filtering applies to both
listing and calling — a tool the model never saw is still refused if it guesses the name.
Tools are namespaced <server>__<tool> so two servers can both have a search without
colliding. Globs always match the canonical name, never the alias, so renaming can never be
used to slip past a deny rule.
Resources and prompts
Both are proxied alongside tools, namespaced the same way. Prompts become <server>__<name>;
resources get a scheme, mcpgw://<server>/<original-uri>, so the backend's own URI survives
whole and comes back intact on the way down.
resources/subscribe is reference-counted: however many sessions watch the same resource, the
backend is subscribed once, and notifications/resources/updated is delivered only to the
sessions that asked — closing a session releases whatever it was the last one holding.
The two are filtered differently, and the difference is deliberate:
Filtered by | |
Tools, prompts | The full policy: server membership, then deny globs, then the allow list |
Resources, templates | Server membership only |
Globs are written against names, and a resource is addressed by URI — matching github__get_*
against mcpgw://github/file:///x would be guesswork. So a profile that can reach a server can
read its resources. If that is too broad for a server you are exposing, keep it out of that
profile's servers list rather than trying to express it as a glob.
Cancellation and logging
A client that cancels a call cancels it all the way down: the abort is carried into the backend request, so the backend stops working rather than finishing into a discarded result. Other sessions sharing that backend are unaffected.
logging/setLevel is recorded per session and never pushed down to the backends — they are
shared, so one client asking for debug would turn it on for everyone. A backend's log messages
are routed to the session whose call they arrived during and filtered by that session's own
level (default info). A message that arrives with no call in flight is dropped rather than
broadcast, for the same reason a reverse request in that state gets -32006.
Audit log
One JSON object per line, in audit/YYYY-MM-DD.jsonl:
{"ts":"2026-08-31T10:12:44.812Z","session":"s_7f3a91","profile":"coding",
"client":{"name":"claude-code","version":"2.1.0"},"method":"tools/call",
"server":"github","tool":"create_issue","exposed_as":"file_bug",
"decision":"allow","args_hash":"sha256:7ab1…","dur_ms":412,"status":"ok"}Plain JSONL, so jq is the query engine:
jq 'select(.decision != "allow")' audit/*.jsonlArguments are hashed by default. Set log_args: full to record them, or none to record
nothing — either way, configured regex patterns are redacted before anything is written.
Tool pinning
The first time a tool is seen, sha256(name + description + inputSchema) goes into
tools.lock.json. Every startup and every tools/list_changed re-checks it. If a server
rewrites a tool description after you approved it, the tool is blocked, the change is logged,
and a diff is printed. mcpgw pin is the only way to accept it.
Commit tools.lock.json.
Known limits
Deliberate, and each one is marked in the code:
Reverse requests need an idle backend. A backend asking the client to sample is routed to the session whose call it arrived during. With two calls in flight on one backend, the second gets
-32006rather than a guess — guessing would leak one client's prompt to another.Rate limits are in memory. Restarting the daemon resets them.
Audit writes are best-effort. A hard crash can lose the last few lines.
tools/listpagination is collapsed into a single page.OAuth is authorization-code only. Client-credentials and device-code flows are not wired, and the callback listens on a fixed
127.0.0.1:8419.
Security
There is no client authentication, by design. The trust boundary is the local machine.
That only holds if the daemon stays local, so the gateway refuses to bind a non-loopback
address unless you explicitly set listen.token. Reaching the port means inheriting every
credential the gateway holds — the interlock is enforced at startup, not left to convention.
Logo
Lines fan in from the left and converge to a single point inside a rounded gate, leaving as one line on the right. Many backends in, one governed path out — and the gate is the only way through.
File | Use |
Horizontal lockup, mark plus wordmark. README and documentation headers | |
Square mark on its own. Favicon, app icon, social card |
Both are transparent PNGs in slate #34494D, which holds up on light and dark backgrounds
alike.
License
MIT
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.
Related MCP Connectors
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Authenticated MCP server for ClearPolicy policy and compliance workflows.
The MCP server that vets MCP servers: identity, risk grade and per-tool risk before you install.
Guarded MCP server for agent-readable business truth, provenance, readiness, and discovery.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceCentralized MCP control plane that proxies multiple upstream MCP servers with tool namespacing, filtering, policy enforcement, audit logging, and health checks.16MIT
- AlicenseNot gradedqualityAmaintenanceA local MCP auth wrapper and credential broker for multi-account workflows, enabling profile switching, secret injection, and policy enforcement for upstream MCP servers.1964MIT
- AlicenseNot gradedqualityBmaintenanceA least-privilege enforcement proxy for MCP servers. It sits between MCP clients and upstream servers, enforcing tool policies, hiding denied tools, requiring human approval for risky actions, and providing a structured audit trail.MIT
- AlicenseNot gradedqualityAmaintenanceAn authorizing reverse proxy for MCP servers that enforces per-call policy rules on tool arguments with audit logging, dry-run, and rate limiting.Apache 2.0
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/vedantnimbarte/mcp-gateway'
If you have feedback or need assistance with the MCP directory API, please join our Discord server