GitHub - ameshkov/mcp-compress-router: Compress all connected MCP into a single router MCP and save up to 99% on tokens

15 min read Original article ↗

MCP Compressing Router

CI npm GitHub release

Compress all connected MCP into a single router MCP and save up to 99% on tokens.

MCP Compress Router

Table of Contents

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_HOME environment 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 -c and credential storage: when you override the config path with -c /some/dir/mcp.json, both credentials.json (OAuth tokens) and tools-cache.json (cached tool schemas) will be stored in that directory — i.e. next to the config file you specified. The .env file, however, is loaded from the configuration directory resolved by MCP_COMPRESS_ROUTER_HOME or the platform default, not from beside the explicit -c path. To co-locate .env with a custom config, set MCP_COMPRESS_ROUTER_HOME to 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 max for 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 medium when tool names alone are ambiguous and a one-line hint helps disambiguate.
  • Use low sparingly — only when full descriptions must be visible without a get_tool_schema call, 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.

  1. Create a GitHub OAuth App. Open https://github.com/settings/developersNew OAuth App (or Register an application). Give it any name and homepage URL.

  2. Configure the callback URL. Set the Authorization callback URL to: http://localhost/mcp-compress-router/oauth-callback

  3. Add the GitHub MCP server by URL.

    npx mcp-compress-router@latest add github https://api.githubcopilot.com/mcp
  4. Set oauth credentials in mcp.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:org covers the common repo and organization operations. Put the actual values in your .env file (see Secrets and Variable Expansion).

  5. Run the login command.

    npx mcp-compress-router@latest login github

    Your browser opens to authorize. After you approve, tokens are stored in credentials.json and 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 GeneralGenerate 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.

  1. 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.

  2. Register the MCP client via Figma's API. The redirect URI must use 127.0.0.1 on a fixed port — the port matters, it is reused on every login. This example uses 19876:

    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_id and client_secret from the response (also note the scope is mcp: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"
    }
  3. Add the Figma MCP server by URL.

    npx mcp-compress-router@latest add --transport http figma https://mcp.figma.com/mcp
  4. Set oauth credentials in mcp.json. Put the client ID and secret from step 2 in the server entry, using the mcp:connect scope and the same fixed port you registered as callbackPort:

    Put the actual values in your .env file (see Secrets and Variable Expansion).

  5. Run the login command.

    npx mcp-compress-router@latest login figma

    Your browser opens to authorize. After you approve, tokens are stored in credentials.json and 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-http

The 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-token

Shell 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:

  1. The agent reads the compact catalog from the get_tool_schema description and identifies which tools it needs.
  2. It calls get_tool_schema to learn the exact parameters.
  3. It calls invoke_tool to 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.