MCP Compressing Router
Compress all connected MCP into a single router MCP and save up to 99% on tokens.
Table of Contents
- The Problem
- The Solution
- Prerequisites
- Quick Start
- Configuration
- Connecting Coding Agents
- How It Works
- Aknowledgements
The Problem
When you have multiple MCPs every request to the LLM will include ALL their tools and descriptions, which can quickly eat up your token limit and increase costs.
Check out this example to understand how quickly and how large it can get. This example represents just 3 popular MCP servers: Notion MCP, Github MCP and Pylance MCP.
The overhead that is created is about 26K tokens, but let's check how much it actually costs you in USD. I will use Opus API pricing for calculation and I'll assume that on average you have a 50-turn coding session (pretty reasonable these days).
- Input:
26K tokens * $5 / 1M = $0.13 - Cache write (caching is not free):
26K tokens * $6.25 / 1M = $0.1625 - Cache read (49 turns):
26K tokens * 49 * $0.50 / 1M = $0.637
So the total overhead on an average coding session is about $0.9275. And that's just for 3 MCPs, imagine if you had more!
The Solution
Instead of sending all the tools and descriptions every time, you can use a
single router MCP that compresses all the connected MCPs into one with just
two tools: get_tool_schema, invoke_tool.
get_tool_schema in the description only has a list of MCP servers, optional
descriptions (you can write them yourself), and a list of tool names for each
MCP server. Here is an example of how the
compressed version looks like, and it takes about 900 tokens.
If we repeat our exercise with the compressed version, the total overhead on an average coding session will be about $0.032175 so we saved about 96.5% on costs!
This is just a basic example with just 3 MCP servers, the more MCP servers you have, the more you save.
Prerequisites
- Node.js 24 or later — the router runs on Node.js and is launched
via
npx, so no separate install step is needed. - A coding agent that supports stdio MCP servers — this covers virtually every modern coding agent (opencode, Claude Code, Codex, GitHub Copilot, Cursor, etc.). The router exposes itself as a single stdio MCP server, so any agent that can spawn a local MCP process works.
Quick Start
The router is published on npm as
mcp-compress-router.
You do not need to install it — just run it with npx:
npx mcp-compress-router@latest add playwright -- npx -y @playwright/mcp
This registers a downstream MCP server named playwright and writes it
to your config file. Repeat for every MCP server
you want to compress.
Then point your coding agent at the router:
npx mcp-compress-router@latest
When started without a subcommand, the router runs the MCP server over
stdio and exposes exactly two tools (get_tool_schema,
invoke_tool) to the agent.
Configuration
The router reads its configuration from a single JSON(C) file that lists
every downstream MCP server to compress. You can edit this file by hand
or use the add / remove / get / list CLI commands.
Config File Location
By default, the config file lives in a platform-specific directory
(mcp.jsonc is preferred over mcp.json when both exist):
- Windows:
%APPDATA%\mcp-compress-router\ - macOS:
~/Library/Application Support/mcp-compress-router/ - Linux:
~/.local/share/mcp-compress-router/
You can override this with:
- The
-c, --config <path>flag on any command, or - The
MCP_COMPRESS_ROUTER_HOMEenvironment variable (points to a directory containing the config file).
If the file does not exist when a management command runs, it is created
automatically with an empty { "mcpServers": {} } body.
A .env file in the same directory is loaded automatically at
startup, so you can keep secrets out of the config (see
Secrets and Variable Expansion).
Note on
-cand credential storage: when you override the config path with-c /some/dir/mcp.json, bothcredentials.json(OAuth tokens) andtools-cache.json(cached tool schemas) will be stored in that directory — i.e. next to the config file you specified. The.envfile, however, is loaded from the configuration directory resolved byMCP_COMPRESS_ROUTER_HOMEor the platform default, not from beside the explicit-cpath. To co-locate.envwith a custom config, setMCP_COMPRESS_ROUTER_HOMEto the same directory.
Adding Downstream Servers
Use the add command to register a downstream MCP server.
A good description helps the LLM route requests to the correct server.
When several servers are compressed behind the router, the model sees
each server's name, its description, and a list of tool names in the
get_tool_schema catalog. A clear description (e.g. "GitHub API tools
for issues, PRs, and repos") steers the model toward the right server
far better than a bare name.
stdio server (a local process):
npx mcp-compress-router@latest add github --description "GitHub API tools" \ -- npx -y @modelcontextprotocol/server-github # With environment variables npx mcp-compress-router@latest add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \ --description "GitHub API tools" \ -- npx -y @modelcontextprotocol/server-github
HTTP server (a remote endpoint; transport auto-detected from the URL):
npx mcp-compress-router@latest add my-http https://localhost:3100/mcp # With a custom header npx mcp-compress-router@latest add my-http \ --header "Authorization: Bearer mytoken" \ https://localhost:3100/mcp
This produces a config file that looks like:
Both .json and .jsonc (JSON with comments and trailing commas) are
supported. CLI commands write plain .json; hand-edited files may use
.jsonc.
Other management commands:
npx mcp-compress-router@latest list # list all servers + auth status npx mcp-compress-router@latest get my-http # show one server's config npx mcp-compress-router@latest remove my-http # remove a server
Per-Server Enable/Disable
Every server entry accepts an optional enabled boolean. When set to
false, the router skips that server entirely at startup — no process
spawn, no network connection, no discovery — and it is absent from the
get_tool_schema catalog. All configuration is preserved so the server
can be turned back on instantly. Omitting enabled (the default) means
enabled, keeping mcp.json clean and fully backward compatible.
Toggle it from the CLI without touching the rest of the config:
npx mcp-compress-router@latest disable github # writes "enabled": false npx mcp-compress-router@latest enable github # removes the field
You can also set it at creation time:
npx mcp-compress-router@latest add archive --disabled -- npx -y server-archive
Per-Server Tool Selection
Two optional fields control which of a server's advertised tools are
exposed to the LLM. Both are arrays of glob patterns
(picomatch syntax: *, ?,
{a,b}, [abc]) matched against bare tool names:
allowedTools— when present, only matching tools are exposed. An empty array ([]) exposes no tools (handy for staging a server while you build the list).disabledTools— removes matching tools from whatever would otherwise be exposed. The denylist wins: a tool matching both lists is blocked.
Filtered tools are hidden from the catalog and hard-rejected by
invoke_tool, so even an LLM that guesses a filtered name cannot
reach the downstream server.
A pattern that matches no real tool is not an error — the router logs a
warning (visible with -v) and continues. A malformed pattern is a
hard error at startup. Set filters at creation time with repeatable
flags:
npx mcp-compress-router@latest add github \ --allowed-tools list_issues \ --allowed-tools get_pull_request \ -- npx -y server-github
Compression Levels
Each server's tools are listed in the get_tool_schema description at a
configurable compressionLevel. The level trades catalog compactness
for routing detail: lower levels give the LLM more information up front
(fewer get_tool_schema round-trips), while higher levels minimize the
per-request token overhead. The full JSON parameter schema is always
available via get_tool_schema regardless of the level — only the
catalog listing changes.
Four levels are supported, from most to least compact:
| Level | Tool listing format | Description shown? |
|---|---|---|
max |
toolA, toolB, toolC (comma-separated, single line) |
No |
high (default) |
toolName(arg1, arg2) (one per line) |
No |
medium |
toolName(arg1, arg2): first sentence... (one per line) |
Snippet |
low |
<tool>toolName(arg1, arg2): full description</tool> (one per line) |
Full |
Argument names are extracted from each tool's inputSchema.properties
keys in definition order. When a tool has no description, the medium
and low listings omit the description portion and show just the
signature.
Omitting compressionLevel (the default) is equivalent to high. Set
it per server in mcp.json:
Or set it at creation time with the --compression-level flag:
npx mcp-compress-router@latest add github \ --compression-level medium \ -- npx -y @modelcontextprotocol/server-github
A good rule of thumb:
- Use
maxfor servers whose tool names are self-describing and you want the smallest possible catalog. - Use
high(the default) for most servers — argument names are usually enough for the LLM to pick the right tool. - Use
mediumwhen tool names alone are ambiguous and a one-line hint helps disambiguate. - Use
lowsparingly — only when full descriptions must be visible without aget_tool_schemacall, since it costs the most tokens.
Inspecting Tools
To see exactly which tools a server advertises — and which are
[exposed] or [filtered] under your current selection — connect to
it live without starting the full router:
npx mcp-compress-router@latest tools github
This works regardless of the server's enabled state (inspecting a
disabled server is the primary way to build its allowlist). For HTTP
servers, stored OAuth credentials and oauth overrides are reused. If
the server cannot be reached or is missing required auth, the command
exits non-zero with a clear error and prints no partial list.
OAuth
HTTP servers that require OAuth are supported. When you add an HTTP
server, the router probes it for OAuth metadata and starts the login
flow automatically if OAuth is advertised. You can also trigger it
manually:
npx mcp-compress-router@latest login my-http
This opens your browser to complete the authorization-code flow. Tokens
are stored in a separate credentials.json in the same directory as
mcp.json (with 0600 permissions on Unix), so you can safely
share or version-control mcp.json without exposing tokens. Cached
tool schemas are stored in tools-cache.json in the same directory.
Add both credentials.json and tools-cache.json to your
.gitignore.
By default the router uses
Dynamic Client Registration.
If your server requires a pre-registered client, add an oauth block to
the server entry (in mcp.json):
Only clientId is required; clientSecret and scope are optional.
Redirect URL
During login the router starts a temporary local HTTP server and uses
a loopback redirect URI (per RFC 8252):
http://localhost:<port>/mcp-compress-router/oauth-callback
<port> is chosen by the OS at login time, so there is no fixed port to
register. When a provider requires a pre-registered redirect URI,
register the loopback form without a port:
http://localhost/mcp-compress-router/oauth-callback
Most providers (GitHub included) match the scheme, host, and path and
ignore the port on localhost. If your provider demands a redirect URI
with an exact port, pin it with --port:
npx mcp-compress-router@latest login my-http --port 8765
This binds the callback server to 8765, so the redirect URI becomes
http://localhost:8765/mcp-compress-router/oauth-callback — register
that exact URL with the provider. To reuse the same port on every
login, persist it in the server's oauth block instead of passing the
flag each time:
--port overrides oauth.callbackPort for a single run. Pass --port 0
to force an OS-assigned port even when oauth.callbackPort is set.
GitHub MCP with OAuth (special case)
The official GitHub MCP server at
https://api.githubcopilot.com/mcp advertises OAuth but does not
support Dynamic Client Registration, so you must pre-register a GitHub
OAuth App and pass its credentials via the oauth block. GitHub also
requires that the OAuth App be installed to the repositories and
organizations you want the MCP to access.
-
Create a GitHub OAuth App. Open https://github.com/settings/developers → New OAuth App (or Register an application). Give it any name and homepage URL.
-
Configure the callback URL. Set the Authorization callback URL to:
http://localhost/mcp-compress-router/oauth-callback -
Add the GitHub MCP server by URL.
npx mcp-compress-router@latest add github https://api.githubcopilot.com/mcp
-
Set
oauthcredentials inmcp.json. Copy the Client ID and generate a Client Secret, then put them in the server entry (use variable expansion to keep secrets out of the file):Request only the scopes the tools you need require;
repo read:orgcovers the common repo and organization operations. Put the actual values in your.envfile (see Secrets and Variable Expansion). -
Run the login command.
npx mcp-compress-router@latest login github
Your browser opens to authorize. After you approve, tokens are stored in
credentials.jsonand the router can call GitHub MCP tools.
Note: if you used a GitHub App (not a classic OAuth App), the App must be installed to the accounts/repos you want to access before login will succeed, and its client secret is generated under General → Generate a new client secret.
Figma MCP with OAuth (special case)
The official Figma MCP server at https://mcp.figma.com/mcp does not
support Dynamic Client Registration through the standard MCP flow.
Instead you register an OAuth client via Figma's REST API using a
Personal Access Token, then pass the resulting credentials through the
oauth block. Figma also requires the redirect URI to use a fixed
port — the port you register is reused on every login, so you must
pin it with oauth.callbackPort.
-
Create a Figma Personal Access Token. Follow https://developers.figma.com/docs/rest-api/personal-access-tokens/ to generate a PAT and export it as
FIGMA_PERSONAL_ACCESS_TOKEN. It is only used to register the MCP client in the next step. -
Register the MCP client via Figma's API. The redirect URI must use
127.0.0.1on a fixed port — the port matters, it is reused on everylogin. This example uses19876:curl -X POST https://api.figma.com/v1/oauth/mcp/register \ -H "Content-Type: application/json" \ -H "X-Figma-Token: $FIGMA_PERSONAL_ACCESS_TOKEN" \ -d '{ "client_name": "Claude Code (figma)", "redirect_uris": ["http://127.0.0.1:19876/mcp-compress-router/oauth-callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }'
Save the
client_idandclient_secretfrom the response (also note thescopeismcp:connect):{ "client_id": "CLIENTID", "client_secret": "CLIENTSECRET", "client_name": "Claude Code (figma)", "redirect_uris": ["http://127.0.0.1:19876/mcp-compress-router/oauth-callback"], "token_endpoint_auth_method": "none", "scope": "mcp:connect" } -
Add the Figma MCP server by URL.
npx mcp-compress-router@latest add --transport http figma https://mcp.figma.com/mcp
-
Set
oauthcredentials inmcp.json. Put the client ID and secret from step 2 in the server entry, using themcp:connectscope and the same fixed port you registered ascallbackPort:Put the actual values in your
.envfile (see Secrets and Variable Expansion). -
Run the login command.
npx mcp-compress-router@latest login figma
Your browser opens to authorize. After you approve, tokens are stored in
credentials.jsonand the router can call Figma MCP tools.
Other OAuth commands:
npx mcp-compress-router@latest logout my-http # remove stored credentials
For headless or CI environments, override the browser with the
MCP_COMPRESS_ROUTER_BROWSER environment variable. The authorization
URL is appended as a single final argument (no shell):
MCP_COMPRESS_ROUTER_BROWSER="node /path/to/headless-browser.js" \
npx mcp-compress-router@latest login my-httpThe default login timeout is 120 seconds; override it with
MCP_COMPRESS_ROUTER_LOGIN_TIMEOUT_MS.
Custom Headers
For HTTP servers that authenticate with a static API key or bearer
token instead of OAuth, use the headers field. You can set it via the
CLI or directly in mcp.json:
npx mcp-compress-router@latest add my-http \ --header "Authorization: Bearer mytoken" \ --header "X-Custom: value" \ https://example.com/mcp
Header values support variable expansion, so you can keep the actual token out of the config file.
Secrets and Variable Expansion
Every string field in a server entry (command, args, env,
headers, url, oauth.*) is expanded against the process
environment at load time. Two syntaxes are supported:
| Syntax | Behavior |
|---|---|
${VAR} |
Replaced with the value of VAR. Throws if unset. |
${VAR:-default} |
Replaced with VAR when set and non-empty, otherwise default. |
Put your secrets in a .env file next to mcp.json:
# <config directory>/.env
GITHUB_PERSONAL_TOKEN=ghp_abc123
MY_SERVER_TOKEN=secret-tokenShell environment variables always take precedence over .env values.
Connecting Coding Agents
Once your downstream servers are configured, connect your agent to the
router the same way you would connect any other MCP server — by
pointing it at npx mcp-compress-router@latest. The examples below assume the
default config location; pass -c <path> if
you use a custom one.
Opencode
opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
Claude Code
claude mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
Codex
codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
GitHub Copilot (VS Code)
Add this to .vscode/mcp.json in your workspace (project-level, applies
only to that workspace), or to your user-level MCP settings which
apply across every workspace: open the Command Palette (Cmd+Shift+P) →
MCP: Open User Configuration and add the same servers
block under the mcp key. Project-level and user-level entries are
merged, with project-level taking precedence.
{
"servers": {
"mcp-compress-router": {
"command": "npx",
"args": ["-y", "mcp-compress-router@latest"]
}
}
}How It Works
Once connected, the agent sees exactly two tools:
get_tool_schema(server, tools)— Retrieves the JSON parameter schema for one or more tools on a downstream MCP server. The tool's description includes a compact listing of all servers and their available tool names.invoke_tool(server, tool, arguments)— Forwards a tool call to the downstream MCP server and returns the result.
The typical workflow:
- The agent reads the compact catalog from the
get_tool_schemadescription and identifies which tools it needs. - It calls
get_tool_schemato learn the exact parameters. - It calls
invoke_toolto execute a tool, validated against the cached schema.
This replaces thousands of tokens of tool listings with a compact ~900 token catalog, regardless of how many downstream servers you have.
For the full configuration and environment variable reference, see configuration.md.
Acknowledgements
- mcp2cli — a very similar idea of how MCP can be compressed, but to a CLI.
- mcp-compressor — also a very similar idea; the "two tools" approach was borrowed from this project, though it only compresses a single MCP server.
