One-Stop MCP is a gateway between one MCP client and any number of MCP servers. Connected tool definitions are re-processed on every model request, so a session's cost grows with how many tools are connected, not with how much work gets done. The gateway holds the client-facing surface at four fixed meta-tools regardless of how many servers sit behind it, and injects a tool's full schema only when a task needs it. Because every server is reached through it, the gateway is also the one place credentials are stored and used.
Every connected MCP server's tool catalog (names, descriptions, JSON schemas) goes into the model's context at session start. Connect enough servers and that is tens of thousands of tokens of schema before you type anything.
Inference re-processes the whole context on every request: system prompt, tool definitions, conversation so far. So you don't pay for tool definitions once. You pay for them on every request, for the entire session, and in an agent loop every tool call round trip is another request.
Your bill therefore scales with how many tools are connected, not with how much work is being done. Most of that spend buys nothing: any given request uses two or three tools out of a hundred.
The problem was found by profiling real Claude Code sessions with /usage: most of each session's token spend was going to re-processing context rather than to the work itself. Tool definitions are the part of that re-processing that grows with every server you connect.
These are assumptions, not measurements. Substitute your own numbers.
| assumption | value |
|---|---|
| Connected servers | 10 |
| Tools per server, average | 12, so 120 tools |
| Tokens per tool definition (name, description, schema) | 300, so 36,000 tokens |
| Model requests in the session, counting tool-call round trips | 50 |
Gateway: find_tools calls | 5, spread through the session, each one an extra request (55 total) |
Gateway: tools returned per find_tools call | 5 (the default k) × 300 tokens = 1,500 tokens |
36,000 tokens × 50 requests
= 1,800,000 tokens
fixed surface 341 × 55 = 18,755 shortlists 1,500 × 160 = 240,000 # a find_tools result stays in the # conversation and is re-read by every # later request: 54+43+32+21+10 = 160 total = 258,755 tokens
| after request | connected directly | through the gateway |
|---|---|---|
| 10 | 360,000 | 16,910 |
| 20 | 720,000 | 47,320 |
| 30 | 1,080,000 | 91,230 |
| 40 | 1,440,000 | 148,640 |
| 50 | 1,800,000 | 219,550 |
| 55 | session ended at 50 | 258,755 |
The percentage matters less than the slope. Connect ten more servers and the direct figure doubles to 3,600,000. The gateway figure does not change, because neither the fixed surface nor the shortlist size depends on how many servers are registered.
find_tools call is an extra model request, and that request re-reads the rest of the conversation too. That overhead is real and is not in the numbers above.tools/list payload (npm run budget), not a tokenizer count.Where a provider caches the stable prefix of a request, re-reading tool definitions is billed at a discount instead of full price. It is still billed on every request, caches expire during pauses, and any change to the connected tool set invalidates the cached prefix. The gateway's four-tool surface caches as well, and it does not change when servers are added.
Some clients already defer tool loading. Where a client or model API has its own tool search, part of this cost is handled there. The gateway does it at the MCP layer, so it works the same with any client, and it is where the credential boundary below lives.
A dozen directly connected servers means credentials spread across a dozen client configs, with no shared rotation, no audit trail, and no single place to revoke access. Once every server sits behind one gateway, every credential is used from one process. That is where the vault and OAuth broker go. Credentials
The gateway is an MCP server to the client and an MCP client to each downstream server.
The client's tools/list returns these four tools and nothing else, at any catalog size. Serialized, they come to ~341 tokens (chars/4 estimate). npm run budget fails if the estimate exceeds 2,000.
find_tools(query, k?)k (default 5) tools with their input schemas.list_servers(filter?)invoke(server, tool, args)connect_server(server)The model calls find_tools("open an issue for this bug").
The gateway reads each server's tool list via tools/list (cached after the first read) and ranks every tool against the query.
The top k tools come back with their schemas, compressed to remove JSON-schema boilerplate. This is the only way downstream schemas enter context.
The model picks one and calls invoke(server, tool, args).
The gateway forwards the call over a pooled session to that server and returns the result.
A sparse leg (BM25) and a dense leg (local sentence embeddings), fused with Reciprocal Rank Fusion. The model makes the final choice from the shortlist. Fallback ladder: exact or keyword match, then vector similarity, then category browse via list_servers; on low confidence, widen the shortlist rather than guess.
Field-weighted keyword overlap with light stemming, and field-weighted Okapi BM25, fused with RRF (k = 30). Matches in the tool name count most, then example utterances and category, then description. The dense leg's interfaces (Embedder, VectorRouter) exist, but no embedding model is wired in. The widen-on-low-confidence fallback is not built.
The remaining misses are synonyms ("book" meaning create an event, "jot down" meaning append) that lexical matching cannot catch, which is the dense leg's job. The benchmark measures ranking only; it spawns no servers.
Registered is not running. No downstream server starts when the gateway starts. A server is spawned by the first call that needs it; its session is pooled and closed after 60 seconds idle. Its tool list is cached for the life of the gateway process, so later searches do not respawn it.
The first find_tools call spawns every registered server once to read its tool list. That is acceptable for a handful of servers and not for thousands. Building the catalog at registration time, so discovery never spawns servers, is design-stage.
Shortlisted schemas pass through a compressor before they are returned. light (the default) removes $schema, $id, $ref, title, examples, $comment, and additionalProperties: false, and keeps types, properties, required, and enums. aggressive also removes descriptions. That is smaller, but it can hurt tool selection; how far to go is an open question.
| connected directly | through the gateway | |
|---|---|---|
| Tool definitions in every request | every tool from every server | 4 meta-tools, ~341 tokens |
| Schemas added per discovery | none (already all present) | up to k, default 5 |
| Client context cost of adding a server | grows | none |
| Where catalog size costs something | client context, on every request | gateway memory and ranking time |
With every server behind the gateway, every credential is used from the gateway. Holding them there, instead of in each client config, is what makes rotation, audit, and revocation possible from one place.
The client calls connect_server("notion").
The gateway reads the server's auth type from its manifest (for example OAuth2).
The gateway starts the OAuth flow and returns a redirect URL to the user.
The user approves in the browser. The callback lands on the gateway, not the client.
Tokens are encrypted and stored in the vault, scoped to that user and server.
Later calls to that server are authenticated by the gateway.
Secrets are never written to client config, never returned to the model, and never logged; they are injected only into the outbound call they authenticate. Around the vault, the design adds a usage ledger (which server, which tool, when, and the result, with bulk access flagged), a kill switch (revoke every session and freeze the vault in one action, or scope it to a server or a user), and one-place rotation (automated for OAuth refresh and for providers with key-management APIs, guided otherwise). Team deployments get per-org scoping, admin MFA, and pluggable backends such as HashiCorp Vault or a cloud KMS.
| component | what exists | not yet |
|---|---|---|
Vaultmodule, tested | AES-256-GCM with a key derived by scrypt from an operator passphrase; secrets scoped to (user, server); encrypted snapshot and reload; freeze(); rotate() replaces a stored secret. | Not wired into the running gateway. Nothing injects vault secrets into outbound calls yet. |
UsageLedgerwired into invoke | Records every invoke (user, server, tool, result). Flags more than 100 calls to the same (user, server, tool) within 60 seconds; both limits configurable. | In-memory only. No persistence and no viewer. |
KillSwitchmodule, tested | Closes all downstream sessions and freezes the vault, or closes one server's session. Each action is recorded in the ledger. | No operator command or UI triggers it. Per-user revocation is not built. |
| OAuth broker design-stage | Nothing yet; connect_server is a stub. | The whole flow above. |
| Rotation manual only | Manual replacement via Vault.rotate(). | Automated refresh and scheduled rotation. |
The design brief names libsodium. The implementation uses Node's built-in crypto to avoid a dependency; heavier backends remain the plan for team deployments.
The gateway runs locally over stdio and proxies a demo server end to end. It is not packaged, not published, and not ready for real credentials.
| area | state | evidence |
|---|---|---|
| Four-tool facade over stdio | built tested | test/gateway.test.ts |
| Startup surface under 2,000 tokens | built ~341 tokens (chars/4 estimate) | npm run budget |
| Downstream proxy: stdio, pooled sessions, 60 s idle timeout | built tested against a demo echo server | test/gateway.test.ts |
| Streamable HTTP, client-facing or downstream | not built manifests accept it; connecting throws | src/registry.ts |
| Lexical hybrid router (overlap + BM25, RRF) | built, below target 85.3% top-3 vs 90% | npm run bench |
| Dense (embedding) routing leg | interfaces only no model chosen | src/router.ts |
| Schema compression | built tested | test/compress.test.ts |
| Vault, usage ledger, kill switch | modules, tested only the ledger is wired into the gateway | test/security.test.ts |
| OAuth broker, credential injection, automated rotation | design-stage | |
| Seed catalog of 50 servers | not started one demo manifest (servers/echo.yaml) | |
| Multi-tenant deployment, observability | not started | |
| npm and Docker packaging | not started package.json is marked private |
The 26 tests (npm test), the budget estimate, and the benchmark result were last recorded on 2026-09-03.
npm install npm run build npm start # gateway on stdio; loads manifests from servers/ npm test # client -> gateway -> echo server over stdio npm run budget # startup-surface size check npm run bench # routing accuracy on the offline benchmark
Point an MCP client at node dist/index.js. The only server included is servers/echo.yaml.
One server proxied end to end over stdio.
At 85.3% top-3; the dense leg is pending.
Vault, ledger, and kill switch built as modules; broker not started.
The initial catalog.
Self-hosted team deployment.
Contribution machinery.
Open design questions (core language, which 50 servers, embedding model, secret isolation, compression level, registry sync, rotation policy, anomaly detection, governance) are in the design brief.
Adding a server is one YAML file in servers/ and one PR. No gateway code changes. The gateway loads every *.yaml there at startup and gets the tool list from the server itself via initialize and tools/list.
id: filesystem name: Filesystem category: [dev, files] description: Read, write, and search files under an allowed directory. transport: type: stdio command: npx args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"] auth: type: none # none | api_key | oauth2 routing: examples: # matched against find_tools queries - "read the config file in my project" - "list the files in the docs folder"
Required fields are id, name, transport.type, and command for stdio or url for streamable-http. A malformed manifest fails at load time with the file name.
stdio connects. streamable-http passes validation but is not supported yet.auth: none works end to end. api_key and oauth2 servers register but cannot authenticate until the broker exists.routing.examples apply to every tool on the server. Per-tool examples are planned.servers/echo.yaml is the only manifest tested end to end. Manifest CI validation is planned (P5), not built.Labeled queries in bench/queries.jsonl, with matching entries in bench/catalog.json. The benchmark is the acceptance gate for routing changes.
An Embedder implementation backed by a local model.
Transport support on both faces of the gateway.
Input on the questions in the design brief.
@modelcontextprotocol/sdk ^1.30). Declares the tools capability and serves tools/list and tools/call.initialize, tools/list, and tools/call over stdio.Conventions on top of the protocol, not extensions to it.
find_tools result and calls go through invoke, so the client cannot validate arguments against the downstream schema (the server still does), and per-tool permission prompts see invoke, not the underlying tool.invoke returns the downstream result as JSON in one text content block. Image, audio, and resource content is not passed through natively, and a downstream isError sits inside that JSON.notifications/tools/list_changed, in either direction.