MCP Gateway is a server aggregation tool that connects multiple Model Context Protocol (MCP) servers into a single gateway, exposing all tools from connected servers through unified search, describe, and invoke interfaces and it exposes only 5 tools.
When connecting an client (Claude Code, Opencode, etc.) to multiple MCP servers, each server lists all its tools. With 10+ MCPs each exposing 10-50 tools, you can easily exceed 500+ tool descriptions in the system prompt:
10 servers × 20 tools each = 200+ tool descriptions
Each tool: 200-500 chars → 40KB-100KB of description just for tool schemas!
This creates two problems:
- Context overflow: Many LLMs hit their context limit before any conversation happens
- Cognitive overload: LLMs struggle to choose the right tool from hundreds of options
MCP Gateway solves this by providing tool search instead of dumping all tool schemas:
┌─────────────┐ gateway.search ┌─────────────────┐ kubernetes::pods_list ┌──────────────────┐
│ AI Client │ ───────────────────► │ MCP Gateway │ ─────────────────────────► │ Kubernetes MCP │
│ │ │ │ │ │
│ │ ◄────────────────────│ │ ◄───────────────────────── │ │
└─────────────┘ pods_list schema └─────────────────┘ pods output └──────────────────┘
MCP Gateway operates as both an MCP client (connecting to upstream servers) and an MCP server (exposing tools to downstream clients):
┌──────────────┐ MCP ┌─────────────────┐ MCP ┌──────────────────┐
│ AI Client │ ◄──────────── │ MCP Gateway │ ◄──────────── │ Upstream Server │
│ (Claude, etc)│ │ (this gateway) │ │ (playwright, │
└──────────────┘ └─────────────────┘ │ kubernetes...) │
└──────────────────┘
- Gateway starts and reads configuration
- For each configured upstream server, Gateway connects via stdio (local) or HTTP/WebSocket (remote)
- Gateway fetches the tool catalog from each server
- All tools are indexed in a unified catalog with search capabilities
- AI clients connect to Gateway and use
gateway.searchto find relevant tools - Only the tools the client actually needs are invoked
You will notice around ~40% reduction of initial token used.
Add to your Claude MCP configuration:
{
"mcpServers": {
"gateway": {
"command": "bunx",
"args": ["@eznix/mcp-gateway@latest"]
}
}
}Add to your OpenCode MCP configuration:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"mcp-gateway": {
"type": "local",
"command": ["bunx", "@eznix/mcp-gateway@latest"]
},
}
}You may append your global AGENTS.md (~/.config/opencode/AGENTS.md) with this template.
You may now just simple enumerate the MCP available.
MCP Gateway reads configuration from a JSON file. By default, it looks for:
- Path provided as first command-line argument
MCP_GATEWAY_CONFIGenvironment variable~/.config/mcp-gateway/config.json
{
"local-server": {
"type": "local",
"command": ["bun", "run", "/path/to/server.ts"],
},
"remote-server": {
"type": "remote",
"url": "https://mcp.example.com",
"enabled": true
},
"websocket-server": {
"type": "remote",
"url": "wss://mcp.example.com/ws",
"enabled": true
}
}Each entry specifies:
type:"local"or"remote"command(local only): Array with command and arguments to spawn the upstream serverurl(remote only): Full URL of the remote MCP servertransport(optional, remote only): Override transport detection ("streamable_http"or"websocket"). Usually auto-detected from URL protocol.environment(local only): Environment variables to pass to the spawned process, with{env:VAR_NAME}substitution supportenabled: Set to false to skip connecting to this server
Local MCP servers support environment variable substitution using {env:VAR_NAME} syntax:
{
"jupyter-lab": {
"type": "local",
"command": ["uvx", "jupyter-mcp-server@latest"],
"environment": {
"JUPYTER_URL": "http://localhost:{env:JUPYTER_PORT}/",
"JUPYTER_TOKEN": "{env:JUPYTER_TOKEN}",
"DEBUG": "true"
}
}
}The {env:VAR_NAME} placeholders are resolved from the current process environment. If an environment variable is not set, it's replaced with an empty string.
Run MCP Gateway in Docker with HTTP transport:
# Build the image
docker build -t mcp-gateway .
# Run with config mounted
docker run -p 3000:3000 \
-v ./examples/config.json:/home/gateway/.config/mcp-gateway/config.json:ro \
mcp-gatewayEndpoints:
| Endpoint | Description |
|---|---|
GET / |
Gateway info and endpoints |
GET /health |
Health check |
/mcp |
MCP protocol endpoint |
Example:
curl http://localhost:3000/
# {"name":"MCP Gateway",...,"endpoints":{"mcp":"/mcp","health":"/health"}}
curl http://localhost:3000/health
# {"status":"ok"}Remote servers are auto-detected based on the URL protocol:
http://orhttps://→ Streamable HTTP (recommended)ws://orwss://→ WebSocket
{
"gh-grep": {
"type": "remote",
"url": "https://mcp.grep.app"
},
"custom-websocket": {
"type": "remote",
"url": "wss://my-server.com/mcp"
}
}Search for tools across all connected servers.
{
query: "kubernetes pods",
limit: 10, // optional, max 50
filters: {
server: "kubernetes", // optional, filter by server name
}
}Returns matching tools with relevance scores. Tools matching in name are boosted.
Get detailed information about a specific tool.
{
id: "kubernetes::pods_list" // format: serverKey::toolName
}Returns the full tool schema including inputSchema.
Execute a tool synchronously and get immediate results.
{
id: "kubernetes::pods_list",
args: { namespace: "default" },
timeoutMs: 30000 // optional, default 30 seconds
}Start an asynchronous tool execution. Returns a job ID for polling.
{
id: "some-server::long-running-tool",
args: { ... },
priority: 10, // optional, higher values run first
timeoutMs: 60000
}Check the status of an async job.
{
jobId: "job_123456789_abc123"
}All gateway tools use the format serverKey::toolName to identify tools:
kubernetes::pods_list
playwright::browser_navigate
github::create_issue
The serverKey is the key name in your configuration file.
- MCPGateway class: Main orchestrator
- Upstream connection manager: Manages connections to MCP servers (stdio for local, HTTP/WebSocket for remote)
- Tool catalog: In-memory index of all available tools with metadata
- Job queue: Handles async tool invocations with priority ordering and concurrency limits (max 3 concurrent by default)
- Search engine: MiniSearch with BM25 scoring and fuzzy matching
The search uses MiniSearch with BM25 ranking:
- BM25 scoring: Relevance algorithm
- Field boosting: Name matches (3x), title matches (2x), description/server matches (1x)
- Fuzzy matching: Handles typos with 0.2 threshold (e.g., "kubenetes" → finds "kubernetes")
- Prefix search: Partial word matching (e.g., "pod" matches "pods_list")
git clone https://github.com/eznix86/mcp-gateway.git
cd mcp-gateway
bun install
# Run locally (stdio transport)
bun run index.ts
# Run with Docker (HTTP transport on port 3000)
bun run docker:build && bun run docker:runMIT License. See the LICENSE.