One-Stop MCP in build
MCP gateway · in active build · not released

Tool definitions are billed
on every request.

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.

4 meta-tools at any catalog size surface ~341 tokens (est.) routing 85.3% top-3 · target 90% 26 tests · stdio only not published
Status

In active build. Nothing is released or published. The proxy, router, and security modules run and are tested locally. The OAuth broker, embedding-based routing, and the seed catalog are design-stage. What is built

01

The problem: you pay for tool definitions on every request

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.

Where this came from

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.

Worked example illustrative

These are assumptions, not measurements. Substitute your own numbers.

assumptionvalue
Connected servers10
Tools per server, average12, so 120 tools
Tokens per tool definition (name, description, schema)300, so 36,000 tokens
Model requests in the session, counting tool-call round trips50
Gateway: find_tools calls5, spread through the session, each one an extra request (55 total)
Gateway: tools returned per find_tools call5 (the default k) × 300 tokens = 1,500 tokens
connected directly
36,000 tokens × 50 requests

= 1,800,000 tokens
through the gateway
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
// illustrative · from the assumptions above
1,800,000
tool-definition tokens, connected directly
258,755
tool-definition tokens, through the gateway
~86%
fewer in this session. $5.40 vs $0.78 at an assumed $3 / million input tokens, no caching
2×
direct cost after connecting ten more servers. Gateway figure unchanged
Tool-definition tokens, accumulated over the session
Illustrative, from the worked example. Hover or focus the chart and use the arrow keys to read values.
Show data table
after requestconnected directlythrough the gateway
10360,00016,910
20720,00047,320
301,080,00091,230
401,440,000148,640
501,800,000219,550
55session ended at 50258,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.

Not counted in this example

  • Each 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.
  • Schema compression is ignored; shortlists are counted at full size.
  • 341 is a chars/4 estimate of the gateway's tools/list payload (npm run budget), not a tokenizer count.
Prompt caching changes the rate, not the shape

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.

What follows from fixing it: credential sprawl

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

02

How it works

The gateway is an MCP server to the client and an MCP client to each downstream server.

// target architecture · parts are design-stage, see Status
AI CLIENT Claude · Cursor · custom agent MCP ONE-STOP MCP GATEWAY meta-tool facade semantic router vault · oauth broker GitHub Postgres Notion Drive …1000sregistered

2.1 · The client-facing surface: four meta-tools

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.

meta-tool
what it does
find_tools(query, k?)
Ranks the whole catalog against a natural-language query and returns the top k (default 5) tools with their input schemas.
list_servers(filter?)
Lists registered servers: id, name, category, description, auth type, and whether a session is running. No schemas.
invoke(server, tool, args)
Calls one tool on one downstream server and returns its result.
connect_server(server)
Starts auth for a server. Today it only reports the auth type declared in the manifest; the broker is design-stage.

2.2 · Just-in-time schema injection

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.

2.3 · Routing

Hybrid retrieval design

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.

Two lexical legs built

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.

// offline benchmark · npm run bench · last recorded 2026-09-03
85.3%
top-3 (29 of 34). Target is 90%
55.9%
top-1
34
paraphrased queries
82
hand-written tool entries from 13 servers

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.

2.4 · Lazy sessions

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.

Current limitation

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.

2.5 · Schema compression

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.

2.6 · What stays constant and what grows

connected directlythrough the gateway
Tool definitions in every requestevery tool from every server4 meta-tools, ~341 tokens
Schemas added per discoverynone (already all present)up to k, default 5
Client context cost of adding a servergrowsnone
Where catalog size costs somethingclient context, on every requestgateway memory and ranking time
03

Credentials: vault and OAuth broker

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.

Design design

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.

Built so far

componentwhat existsnot yet
Vault
module, 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.
UsageLedger
wired 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.
KillSwitch
module, 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.

04

Status

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.

areastateevidence
Four-tool facade over stdiobuilt testedtest/gateway.test.ts
Startup surface under 2,000 tokensbuilt ~341 tokens (chars/4 estimate)npm run budget
Downstream proxy: stdio, pooled sessions, 60 s idle timeoutbuilt tested against a demo echo servertest/gateway.test.ts
Streamable HTTP, client-facing or downstreamnot built manifests accept it; connecting throwssrc/registry.ts
Lexical hybrid router (overlap + BM25, RRF)built, below target 85.3% top-3 vs 90%npm run bench
Dense (embedding) routing leginterfaces only no model chosensrc/router.ts
Schema compressionbuilt testedtest/compress.test.ts
Vault, usage ledger, kill switchmodules, tested only the ledger is wired into the gatewaytest/security.test.ts
OAuth broker, credential injection, automated rotationdesign-stage
Seed catalog of 50 serversnot started one demo manifest (servers/echo.yaml)
Multi-tenant deployment, observabilitynot started
npm and Docker packagingnot 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.

Running it locally (development only)

from the repo root
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.

05

Roadmap

P0Coredone

Four-tool facade

One server proxied end to end over stdio.

exit → startup surface under 2k tokens
P1Discoveryin progress

Manifests, router, just-in-time injection

At 85.3% top-3; the dense leg is pending.

exit → 90% top-3 on the routing benchmark
P2Authin progress

Vault, OAuth flow, ledger, rotation, kill switch

Vault, ledger, and kill switch built as modules; broker not started.

exit → an OAuth server activated entirely inside the gateway
P3Seednot started

50 server manifests, registry sync

The initial catalog.

exit → useful on first run
P4Teamnot started

Multi-tenant, per-user secrets, metrics

Self-hosted team deployment.

exit → self-hosted multi-user deployment
P5Communitynot started

Docs, manifest CI validation, RFC process, governance

Contribution machinery.

exit → external PRs merging

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.

06

Contributing: add a server with one file

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.

servers/filesystem.yaml
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.

What works today

  • Only stdio connects. streamable-http passes validation but is not supported yet.
  • Only 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.

Other useful contributions

Benchmark queries

Labeled queries in bench/queries.jsonl, with matching entries in bench/catalog.json. The benchmark is the acceptance gate for routing changes.

Dense routing leg

An Embedder implementation backed by a local model.

Streamable HTTP

Transport support on both faces of the gateway.

Open questions

Input on the questions in the design brief.

07

Non-goals

  • Building MCP servers. The gateway aggregates existing ones.
  • Changing or extending the MCP protocol. Everything is built from standard messages.
  • A hosted service. The target is self-hosted. The design should not rule out a hosted version, but none is planned.
  • A fine-tuned routing model. Off-the-shelf retrieval and embeddings first.
  • Shrinking tool results. The gateway reduces the cost of tool definitions. What a tool returns passes through.
08

MCP spec compliance

Conforms current

  • Toward the client, a standard MCP server built on the official TypeScript SDK (@modelcontextprotocol/sdk ^1.30). Declares the tools capability and serves tools/list and tools/call.
  • Toward each downstream server, a standard MCP client: initialize, tools/list, and tools/call over stdio.
  • No custom methods, capabilities, or message fields. The meta-tools are ordinary MCP tools.

Differs from a direct connection

Conventions on top of the protocol, not extensions to it.

  • Downstream tools are not registered with the client. Schemas arrive as JSON text in a 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.

Not supported yet

  • Resources, prompts, sampling, elicitation, roots, and notifications/tools/list_changed, in either direction.
  • Streamable HTTP on either side.
  • The MCP authorization flow for HTTP servers. Credential handling is the design-stage vault and broker.