Skip to content

Commit 4763f2c

Browse files
committed
initial release: local MCP gateway with lazy schema loading
Ship v0.1.0: - fans out to N downstream MCP servers over stdio - namespaces tools with <server>__<tool> to prevent collisions - alwaysExpose: true | false | string[] — only pre-load what you actually use - token-report subcommand estimates context cost per downstream - init / validate / status / add-server / start / help-agents CLI - agent-friendly: --json envelope, stable exit codes, machine catalog - 11 vitest tests, incl. in-memory transport integration tests
0 parents  commit 4763f2c

22 files changed

Lines changed: 4152 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
node: [20, 22]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: pnpm/action-setup@v4
18+
with:
19+
version: 10
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: ${{ matrix.node }}
23+
cache: pnpm
24+
- run: pnpm install --frozen-lockfile
25+
- run: pnpm run typecheck
26+
- run: pnpm run build
27+
- run: pnpm run test

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
spec.md
2+
dist/
3+
*.tsbuildinfo
4+
node_modules/
5+
coverage/
6+
.vitest/
7+
.env
8+
.env.*
9+
!.env.example
10+
.DS_Store

.npmignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
spec.md
2+
src/
3+
tests/
4+
examples/
5+
tsup.config.ts
6+
tsconfig.json
7+
vitest.config.ts
8+
.github/
9+
.gitignore
10+
.vitest/
11+
coverage/
12+
*.tsbuildinfo

AGENTS.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# mcp-gateway — agent-facing reference
2+
3+
Machine-friendly spec for driving `mcp-gateway` from a coding agent (Claude Code, Codex, Cursor, Cline, Aider, Copilot, etc.).
4+
5+
**Tip:** Run `mcp-gateway help-agents` (or any other command with `--json`) for the full catalog in one line of JSON.
6+
7+
## Global flags
8+
9+
| Flag | Type | Effect |
10+
|---|---|---|
11+
| `--json` | boolean | Emit a one-line JSON envelope on stdout |
12+
| `--quiet` | boolean | Suppress stderr logs |
13+
| `--verbose` | boolean | More detail on stderr |
14+
| `--cwd <path>` | path | Override the working directory |
15+
| `--config <path>` | path | Config file (default `./mcp-gateway.config.json`) |
16+
17+
## JSON envelope
18+
19+
```json
20+
{"ok": true, "data": {...}}
21+
```
22+
23+
Error:
24+
25+
```json
26+
{"ok": false, "error": {"code": "E_VALIDATION", "message": "...", "hint": "..."}}
27+
```
28+
29+
Stable error codes: `E_VALIDATION`, `E_INTERNAL`.
30+
31+
Exit codes: `0` success, `1` user error, `2` internal error.
32+
33+
## Config schema
34+
35+
```ts
36+
{
37+
version: 1,
38+
namespaceSeparator: string, // default "__"
39+
servers: [
40+
{
41+
name: string, // lowercase alphanumeric + underscore, used as namespace prefix
42+
command: string, // executable
43+
args?: string[],
44+
env?: Record<string,string>,
45+
cwd?: string,
46+
alwaysExpose: boolean | string[], // true | false | list of tool names to pre-expose
47+
enabled?: boolean, // default true
48+
description?: string
49+
}
50+
]
51+
}
52+
```
53+
54+
## Commands
55+
56+
### `start`
57+
58+
Run the gateway as a stdio MCP server. Your upstream client (Claude Code, Cursor, etc.) connects to this single process instead of each individual downstream server. Returns a streaming MCP session, not JSON.
59+
60+
### `status`
61+
62+
Connect to each enabled downstream, list tools, then disconnect. Emit a snapshot.
63+
64+
```json
65+
{
66+
"ok": true,
67+
"data": {
68+
"config": {"path": "...", "servers": 3},
69+
"downstreams": [
70+
{"name": "fs", "status": "ready", "enabled": true, "alwaysExpose": true, "tools": 12, "lastError": null}
71+
]
72+
}
73+
}
74+
```
75+
76+
### `token-report`
77+
78+
Estimate tokens per downstream's tool schemas.
79+
80+
```json
81+
{
82+
"ok": true,
83+
"data": {
84+
"totalExposedTokens": 4800,
85+
"totalAvailableTokens": 61200,
86+
"servers": [
87+
{"name": "fs", "exposed": true, "alwaysExposed": true, "tokens": 4800, "tools": [{"name": "fs__read_file", "tokens": 380}]}
88+
]
89+
}
90+
}
91+
```
92+
93+
### `validate`
94+
95+
Validate the config without connecting to downstreams. Exits `1` on any schema or file error.
96+
97+
### `add-server <name> <command> [args...]`
98+
99+
Append a server to the config. With `--write` it modifies the file; without, it prints the resulting config to stdout.
100+
101+
Flags: `--always-expose <tools>` (`all` | `none` | comma-separated tool names), `--write`.
102+
103+
### `init`
104+
105+
Create a starter `mcp-gateway.config.json`. `--write` writes to disk (refuses to overwrite existing file); without, prints to stdout.
106+
107+
### `help-agents`
108+
109+
Returns the full CLI catalog as JSON. Preferred discovery entry point for agents.
110+
111+
## Tool namespacing
112+
113+
Every downstream tool is exposed as `<server_name><separator><tool_name>` — e.g. `fs__read_file`. This prevents collisions and lets the gateway route `call_tool` by looking at the prefix. The separator is configurable (`namespaceSeparator`) but must be consistent across the gateway's lifetime.
114+
115+
## Lazy exposure semantics
116+
117+
- `alwaysExpose: true` — gateway connects at startup and exposes every tool.
118+
- `alwaysExpose: false` — gateway does not expose any tool from the server until the upstream client calls `call_tool` with that server's prefix. At that moment the gateway connects, fetches the schema, and fulfills the call.
119+
- `alwaysExpose: [tool_a, tool_b]` — only the listed tools appear in `tools/list` replies, even though the server is connected. This is useful for large servers (like GitHub's) where most of the surface is noise for a specific project.

CONTRIBUTING.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Contributing
2+
3+
## Local dev
4+
5+
```bash
6+
pnpm install
7+
pnpm run build
8+
pnpm run test
9+
pnpm run typecheck
10+
```
11+
12+
## Testing strategy
13+
14+
- **Unit tests** cover config validation and token math.
15+
- **Integration tests** exercise the `DownstreamManager` against real MCP servers via `@modelcontextprotocol/sdk`'s `InMemoryTransport`. No child processes are spawned during tests — the test plants a real `Server` and `Client` in memory.
16+
17+
If you're adding code that interacts with downstream servers, prefer writing the test as an in-memory integration test rather than mocking the SDK.
18+
19+
## Adding a new exposure mode
20+
21+
Today we support `alwaysExpose: true | false | string[]`. If you add a new mode (e.g. `alwaysExpose: { match: "regex" }`):
22+
23+
1. Extend `serverSpecSchema` in `src/config.ts`.
24+
2. Update `resolvedServerAlwaysExposed` and the gateway's `tokenReport` logic.
25+
3. Cover the new shape in `tests/config.test.ts` + `tests/gateway.test.ts`.
26+
4. Document in `AGENTS.md`.
27+
28+
## Style
29+
30+
- Stderr for logs, stdout for data.
31+
- Every command must support `--json` and update the `help-agents` catalog.
32+
- Keep the CLI non-interactive.
33+
- Don't call into the real MCP SDK transport layer from unit tests — use `InMemoryTransport`.
34+
35+
## Commit style
36+
37+
```
38+
config: allow regex match on alwaysExpose
39+
server: fix double-connect on lazy prefix hit
40+
tokens: bump chars/token ratio to match Claude tokenizer
41+
```

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 SwarmClaw AI
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# mcp-gateway
2+
3+
> Local MCP gateway that fans out to N downstream MCP servers, namespaces their tools, and lazy-loads their schemas — so your coding agent's context isn't eaten by MCP boilerplate.
4+
5+
[![npm version](https://img.shields.io/npm/v/@swarmclawai/mcp-gateway.svg)](https://www.npmjs.com/package/@swarmclawai/mcp-gateway)
6+
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
7+
[![CI](https://github.com/swarmclawai/mcp-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/swarmclawai/mcp-gateway/actions/workflows/ci.yml)
8+
9+
## Why this exists
10+
11+
Install more than a handful of MCP servers and something ugly happens: every one of them dumps its full tool schema into your coding agent's context at startup. People routinely report **30,000 – 60,000 tokens** of MCP boilerplate consumed before they've typed a single message. The 1M-context upgrade makes this worse, not better — people just install more servers.
12+
13+
Existing tooling solves the wrong half:
14+
15+
- Registries (wong2/awesome-mcp-servers, mcp.so, smithery, glama) solve **discovery**.
16+
- Docker's own gateway is great at **multi-tenancy** and **auth**.
17+
- Nobody owns the **local runtime** problem: "I have 15 MCP servers installed. I want 3 of them exposed by default and the other 12 to only show up when I actually ask for them."
18+
19+
`mcp-gateway` is that tool. You point your upstream client (Claude Code, Cursor, Cline, Aider, Windsurf, etc.) at one MCP endpoint — the gateway. It fans out to all your downstream servers, prefixes their tool names to prevent collisions, and only exposes the tools you've explicitly chosen to pre-load.
20+
21+
## 30-second demo
22+
23+
```bash
24+
# Generate a starter config
25+
npx @swarmclawai/mcp-gateway@latest init --write
26+
27+
# Edit mcp-gateway.config.json — set alwaysExpose per server
28+
29+
# See how many tokens each server is spending
30+
npx @swarmclawai/mcp-gateway token-report
31+
32+
# Point Claude Code at the gateway instead of at each server individually
33+
claude mcp add gateway -- npx -y @swarmclawai/mcp-gateway@latest start
34+
```
35+
36+
## Config
37+
38+
A single `mcp-gateway.config.json` at your project root (or `--config <path>`):
39+
40+
```json
41+
{
42+
"version": 1,
43+
"namespaceSeparator": "__",
44+
"servers": [
45+
{
46+
"name": "fs",
47+
"command": "npx",
48+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
49+
"alwaysExpose": true
50+
},
51+
{
52+
"name": "github",
53+
"command": "docker",
54+
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
55+
"alwaysExpose": false
56+
},
57+
{
58+
"name": "sentry",
59+
"command": "npx",
60+
"args": ["-y", "@sentry/mcp-server@latest"],
61+
"alwaysExpose": ["issue_details"]
62+
}
63+
]
64+
}
65+
```
66+
67+
- `alwaysExpose: true` — every tool from this server is in your agent's context on startup.
68+
- `alwaysExpose: false` — tools aren't exposed at first; the gateway connects to the server when the agent calls a tool whose name begins with `<prefix>__`.
69+
- `alwaysExpose: ["tool_a", "tool_b"]` — only the listed tools are pre-exposed.
70+
71+
The gateway prefixes every downstream tool with its server name and the namespace separator (`__` by default) so two servers can both expose a tool called `read_file` without collision — your agent sees `fs__read_file` and `github__read_file`.
72+
73+
## Install
74+
75+
```bash
76+
pnpm add -g @swarmclawai/mcp-gateway
77+
# or
78+
npm i -g @swarmclawai/mcp-gateway
79+
# or run on demand
80+
npx @swarmclawai/mcp-gateway@latest --help
81+
```
82+
83+
## Commands
84+
85+
| Command | Purpose |
86+
|---|---|
87+
| `init` | Create a starter config |
88+
| `validate` | Validate the config file without connecting to any downstream |
89+
| `status` | Connect to every enabled downstream and report status + tool counts |
90+
| `token-report` | Estimate how many tokens each downstream's schemas cost |
91+
| `add-server <name> <command> [args...]` | Append a server to the config |
92+
| `start` | Start the gateway (stdio MCP server for an upstream client) |
93+
| `help-agents` | Print the machine-readable command catalog |
94+
95+
Every command accepts `--json` and returns a one-line JSON envelope. Exit codes: `0` success, `1` user error, `2` internal error.
96+
97+
## How token-report works
98+
99+
The report walks every downstream server, connects to it over stdio, calls `tools/list`, and estimates the token cost of each tool's name + description + input schema. We don't call a real tokenizer — that'd introduce a heavy dep for a directional number. Chars / 3.5 is close enough to tell you which server is blowing up your window.
100+
101+
## Wiring it into your agent
102+
103+
### Claude Code
104+
105+
```bash
106+
claude mcp add gateway -- npx -y @swarmclawai/mcp-gateway@latest start
107+
```
108+
109+
Remove your individual server entries — the gateway replaces them.
110+
111+
### Cursor
112+
113+
In `~/.cursor/mcp.json`:
114+
115+
```json
116+
{
117+
"mcpServers": {
118+
"gateway": {
119+
"command": "npx",
120+
"args": ["-y", "@swarmclawai/mcp-gateway@latest", "start"]
121+
}
122+
}
123+
}
124+
```
125+
126+
### Cline, Aider, Windsurf
127+
128+
Same pattern: one `mcp-gateway start` entry in place of N individual server entries. See [`awesome-mcp-for-coding-agents`](https://github.com/swarmclawai/awesome-mcp-for-coding-agents#how-to-install) for the exact config syntax per agent.
129+
130+
## Built for coding agents
131+
132+
Every swarmclawai CLI follows the same agent conventions so Claude Code, Cursor, Cline, Aider, Codex et al can drive them without guessing:
133+
134+
- `--json` everywhere, one-line envelope on stdout
135+
- Stderr for logs, stdout for data
136+
- Stable exit codes: `0` / `1` / `2`
137+
- Non-interactive by default
138+
- `mcp-gateway help-agents` returns the entire command catalog as JSON
139+
140+
See [`AGENTS.md`](./AGENTS.md) for the full machine-readable reference.
141+
142+
## Roadmap
143+
144+
- Session-scoped explicit exposure: an agent can ask the gateway "expose github__* for the rest of this session" without restarting
145+
- Schema compression: strip optional descriptions on lazy-exposed tools to shrink `tools/list` replies further
146+
- Observability endpoint: count tool calls per server to justify what should actually be alwaysExpose
147+
- Remote (HTTP/SSE) transport for upstream clients, not just stdio
148+
- Per-tool deny/allow list beyond the name prefix
149+
150+
## Contributing
151+
152+
See [`CONTRIBUTING.md`](./CONTRIBUTING.md).
153+
154+
## License
155+
156+
[MIT](./LICENSE)

0 commit comments

Comments
 (0)