mcp-compress-router 1.5.1 → 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,9 @@
20
20
  - [The Solution](#the-solution)
21
21
  - [Prerequisites](#prerequisites)
22
22
  - [Quick Start](#quick-start)
23
+ - [1. Connect your coding agent](#1-connect-your-coding-agent)
24
+ - [2. Add downstream MCP servers](#2-add-downstream-mcp-servers)
25
+ - [3. Verify and use](#3-verify-and-use)
23
26
  - [Configuration](#configuration)
24
27
  - [Config File Location](#config-file-location)
25
28
  - [Adding Downstream Servers](#adding-downstream-servers)
@@ -29,17 +32,12 @@
29
32
  - [Inspecting Tools](#inspecting-tools)
30
33
  - [OAuth](#oauth)
31
34
  - [Redirect URL](#redirect-url)
32
- - [GitHub MCP (special case)](#github-mcp-special-case)
33
- - [Figma MCP (special case)](#figma-mcp-special-case)
35
+ - [GitHub MCP with OAuth (special case)](#github-mcp-with-oauth-special-case)
36
+ - [Figma MCP with OAuth (special case)](#figma-mcp-with-oauth-special-case)
34
37
  - [Custom Headers](#custom-headers)
35
38
  - [Secrets and Variable Expansion](#secrets-and-variable-expansion)
36
- - [Connecting Coding Agents](#connecting-coding-agents)
37
- - [Opencode](#opencode)
38
- - [Claude Code](#claude-code)
39
- - [Codex](#codex)
40
- - [GitHub Copilot](#github-copilot)
41
39
  - [How It Works](#how-it-works)
42
- - [Aknowledgements](#acknowledgements)
40
+ - [Acknowledgements](#acknowledgements)
43
41
 
44
42
  ## The Problem
45
43
 
@@ -95,17 +93,99 @@ have, the more you save.
95
93
 
96
94
  The router is published on npm as
97
95
  [`mcp-compress-router`](https://www.npmjs.com/package/mcp-compress-router).
98
- You do not need to install it — just run it with `npx`:
96
+ You do not need to install it — just run it with `npx`.
97
+
98
+ Setup is two steps: first connect your coding agent to the router, then
99
+ add the MCP servers you want to compress behind it.
100
+
101
+ ### 1. Connect your coding agent
102
+
103
+ Point your agent at the router the same way you would point it at any
104
+ other MCP server. The router reads its server list from a
105
+ [user-wide config file](#config-file-location) by default, so register
106
+ it at the **user** scope (or your agent's equivalent) — every project
107
+ then gets the same compressed catalog with no per-project setup.
108
+
109
+ - **Claude Code** — see the
110
+ [Claude Code MCP docs](https://code.claude.com/docs/en/mcp).
111
+ Add it at `user` scope so it applies everywhere (Claude Code also
112
+ supports `local` for a single private project and `project` for a
113
+ shareable `.mcp.json`):
114
+
115
+ ```sh
116
+ claude mcp add mcp-compress-router --scope user -- npx -y mcp-compress-router@latest
117
+ ```
118
+
119
+ - **Opencode** — see the
120
+ [opencode MCP servers docs](https://opencode.ai/docs/mcp-servers/).
121
+
122
+ ```sh
123
+ opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
124
+ ```
125
+
126
+ - **Codex** — see the
127
+ [Codex MCP docs](https://developers.openai.com/codex/mcp).
128
+
129
+ ```sh
130
+ codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
131
+ ```
132
+
133
+ - **GitHub Copilot (VS Code)** — see the
134
+ [VS Code MCP docs](https://code.visualstudio.com/docs/agent-customization/mcp-servers).
135
+ Open the Command Palette (`Cmd+Shift+P`) →
136
+ `MCP: Open User Configuration` and add the server block under
137
+ `mcp.servers` (project-level `.vscode/mcp.json` is also supported
138
+ and merged with the user-level settings, project taking precedence):
139
+
140
+ ```json
141
+ {
142
+ "servers": {
143
+ "mcp-compress-router": {
144
+ "command": "npx",
145
+ "args": ["-y", "mcp-compress-router@latest"]
146
+ }
147
+ }
148
+ }
149
+ ```
150
+
151
+ ### 2. Add downstream MCP servers
152
+
153
+ Use the `add` command to register each MCP server you want to compress.
154
+ The router writes them to its [config file](#config-file-location) and
155
+ they appear in the catalog the next time the router starts.
99
156
 
100
157
  ```bash
101
158
  npx mcp-compress-router@latest add playwright -- npx -y @playwright/mcp
102
159
  ```
103
160
 
104
- This registers a downstream MCP server named `playwright` and writes it
105
- to your [config file](#config-file-location). Repeat for every MCP server
106
- you want to compress.
161
+ Add a description so the LLM can pick the right server, and use
162
+ environment variables to keep secrets out of the config:
163
+
164
+ ```bash
165
+ npx mcp-compress-router@latest add github \
166
+ --description "GitHub API tools" \
167
+ -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
168
+ -- npx -y @modelcontextprotocol/server-github
169
+ ```
107
170
 
108
- Then point your [coding agent](#connecting-coding-agents) at the router:
171
+ Repeat for every MCP server you want to compress. See
172
+ [Adding Downstream Servers](#adding-downstream-servers) for HTTP
173
+ servers, custom headers, OAuth, and other options.
174
+
175
+ ### 3. Verify and use
176
+
177
+ Check that everything is wired up:
178
+
179
+ ```bash
180
+ npx mcp-compress-router@latest list
181
+ ```
182
+
183
+ Then start a new session in your coding agent. The agent picks up the
184
+ router's two tools — `get_tool_schema` and `invoke_tool` — and uses
185
+ them to discover and call every server you added, without the agent
186
+ ever seeing the raw tool listings of each downstream server.
187
+
188
+ Under the hood your agent spawns the router as a child process with:
109
189
 
110
190
  ```bash
111
191
  npx mcp-compress-router@latest
@@ -113,7 +193,8 @@ npx mcp-compress-router@latest
113
193
 
114
194
  When started without a subcommand, the router runs the MCP server over
115
195
  stdio and exposes exactly two tools (`get_tool_schema`,
116
- `invoke_tool`) to the agent.
196
+ `invoke_tool`) to the agent. You normally do not run this yourself —
197
+ your agent spawns the router automatically.
117
198
 
118
199
  ## Configuration
119
200
 
@@ -338,6 +419,16 @@ A good rule of thumb:
338
419
  - Use `low` sparingly — only when full descriptions must be visible
339
420
  without a `get_tool_schema` call, since it costs the most tokens.
340
421
 
422
+ > **Note for Claude Code:** Claude Code truncates any single tool
423
+ > description at 2000 characters. The router renders the entire
424
+ > compressed catalog into the `get_tool_schema` description that the
425
+ > agent receives on every turn, so with several downstream servers or
426
+ > many tools per server the `high`, `medium`, or `low` listings can
427
+ > exceed that limit and get truncated — breaking routing. When using
428
+ > Claude Code, set `compressionLevel: "max"` on your servers (or pass
429
+ > `--compression-level max` to `add`) so each server's tool listing
430
+ > collapses to a comma-separated single line.
431
+
341
432
  ### Inspecting Tools
342
433
 
343
434
  To see exactly which tools a server advertises — and which are
@@ -640,52 +731,6 @@ MY_SERVER_TOKEN=secret-token
640
731
 
641
732
  Shell environment variables always take precedence over `.env` values.
642
733
 
643
- ## Connecting Coding Agents
644
-
645
- Once your downstream servers are configured, connect your agent to the
646
- router the same way you would connect any other MCP server — by
647
- pointing it at `npx mcp-compress-router@latest`. The examples below assume the
648
- default [config location](#config-file-location); pass `-c <path>` if
649
- you use a custom one.
650
-
651
- ### Opencode
652
-
653
- ```sh
654
- opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
655
- ```
656
-
657
- ### Claude Code
658
-
659
- ```sh
660
- claude mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
661
- ```
662
-
663
- ### Codex
664
-
665
- ```sh
666
- codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
667
- ```
668
-
669
- ### GitHub Copilot (VS Code)
670
-
671
- Add this to `.vscode/mcp.json` in your workspace (project-level, applies
672
- only to that workspace), or to your **user-level** MCP settings which
673
- apply across every workspace: open the Command Palette (`Cmd+Shift+P`) →
674
- `MCP: Open User Configuration` and add the same `servers`
675
- block under the `mcp` key. Project-level and user-level entries are
676
- merged, with project-level taking precedence.
677
-
678
- ```json
679
- {
680
- "servers": {
681
- "mcp-compress-router": {
682
- "command": "npx",
683
- "args": ["-y", "mcp-compress-router@latest"]
684
- }
685
- }
686
- }
687
- ```
688
-
689
734
  ## How It Works
690
735
 
691
736
  Once connected, the agent sees exactly **two tools**:
@@ -17,7 +17,7 @@ function validateToolListPatterns(field, patterns) {
17
17
  }
18
18
  catch (err) {
19
19
  const reason = err instanceof Error ? err.message : String(err);
20
- throw new Error(`Invalid "${field}" pattern "${pattern}": ${reason}`);
20
+ throw new Error(`Invalid "${field}" pattern "${pattern}": ${reason}`, { cause: err });
21
21
  }
22
22
  }
23
23
  }
@@ -3,7 +3,7 @@ import { handleAdd, handleRemove, handleGet, handleList, handleLogin, handleLogo
3
3
  import { runRouter } from './router-runner.js';
4
4
  /**
5
5
  * Collects repeated `--header "K: V"` flags into a headers record.
6
- * @internal — Exported for tests only; not part of the public module API.
6
+ * @internal Exported for tests only; not part of the public module API.
7
7
  */
8
8
  export function collectHeaders(value, previous) {
9
9
  const colonIdx = value.indexOf(':');
@@ -16,7 +16,7 @@ export function collectHeaders(value, previous) {
16
16
  }
17
17
  /**
18
18
  * Collects repeated `-e KEY=value` flags into an env record.
19
- * @internal — Exported for tests only; not part of the public module API.
19
+ * @internal Exported for tests only; not part of the public module API.
20
20
  */
21
21
  export function collectEnv(value, previous) {
22
22
  const eqIdx = value.indexOf('=');
@@ -29,7 +29,7 @@ export function collectEnv(value, previous) {
29
29
  }
30
30
  /**
31
31
  * Collects repeated `--flag <value>` flags into an ordered string array.
32
- * @internal — Exported for tests only; not part of the public module API.
32
+ * @internal Exported for tests only; not part of the public module API.
33
33
  */
34
34
  export function collectStringArray(value, previous) {
35
35
  return [...previous, value];
@@ -38,7 +38,7 @@ export function collectStringArray(value, previous) {
38
38
  * Coerces a `--port` flag value into an integer. Throws on non-numeric
39
39
  * values so the user gets a clear error before any network activity.
40
40
  * Range validation is deferred to the command handlers.
41
- * @internal — Exported for tests only; not part of the public module API.
41
+ * @internal Exported for tests only; not part of the public module API.
42
42
  */
43
43
  export function parsePort(value) {
44
44
  const port = Number(value);
@@ -20,3 +20,76 @@ export class GuidedAuthError extends Error {
20
20
  this.serverName = serverName;
21
21
  }
22
22
  }
23
+ /**
24
+ * HTTP status codes that indicate an authentication or authorization
25
+ * failure at the transport layer. The MCP SDK's streamable HTTP
26
+ * transport surfaces the upstream response status as a numeric `code`
27
+ * property on the thrown error — e.g. 401 when the server rejects a
28
+ * request without driving the SDK's `redirectToAuthorization` flow.
29
+ */
30
+ const AUTH_ERROR_STATUSES = new Set([401, 403]);
31
+ /**
32
+ * Error message substrings that indicate a downstream OAuth /
33
+ * access-token failure rather than a network, transport, or tool-level
34
+ * error. These surface when a server rejects a request in the response
35
+ * body (e.g. with an OAuth error code) instead of returning a clean 401
36
+ * that would drive the SDK's `redirectToAuthorization` flow (which
37
+ * throws {@link GuidedAuthError} directly).
38
+ *
39
+ * Matching is case-insensitive to tolerate casing differences across
40
+ * SDK versions and server implementations.
41
+ */
42
+ const AUTH_ERROR_PATTERNS = [
43
+ 'invalid_token',
44
+ 'invalid_grant',
45
+ 'invalid_client',
46
+ 'missing or invalid access token',
47
+ ];
48
+ /**
49
+ * Reads the HTTP status code attached to an error, if any.
50
+ *
51
+ * The MCP SDK's `StreamableHTTPError` exposes the upstream HTTP status
52
+ * as a numeric `code` property. JSON-RPC error codes (e.g. `-32601`
53
+ * Method not found) are negative and never collide with HTTP status
54
+ * codes, so a positive value here is unambiguously an HTTP status.
55
+ * Duck-typed to avoid importing the SDK error class (mirrors the
56
+ * `isMethodNotFound` approach in `discovery.ts`).
57
+ *
58
+ * @param err - The thrown value from connect, reconnect, or invoke.
59
+ * @returns The HTTP status code, or `undefined` when none is present.
60
+ */
61
+ function getHttpStatus(err) {
62
+ if (typeof err === 'object' && err !== null && 'code' in err) {
63
+ const code = err.code;
64
+ return typeof code === 'number' ? code : undefined;
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Determines whether an error represents an authentication failure
70
+ * (missing, invalid, or expired OAuth credentials) rather than a
71
+ * network, transport, or tool-level error.
72
+ *
73
+ * Returns true for {@link GuidedAuthError} instances, for raw
74
+ * transport errors carrying an HTTP 401/403 status (read from the
75
+ * `code` property), and for errors whose message carries an OAuth
76
+ * error code such as `invalid_token` or `invalid_grant`. Without
77
+ * this, a server that rejects a request is misclassified as a generic
78
+ * connection failure, and the guided error tells the user to check
79
+ * their network instead of running `login`.
80
+ *
81
+ * @param err - The error thrown during connect, reconnect, or invoke.
82
+ * @returns True when the error indicates authentication is required.
83
+ */
84
+ export function isAuthError(err) {
85
+ if (err instanceof GuidedAuthError) {
86
+ return true;
87
+ }
88
+ const status = getHttpStatus(err);
89
+ if (status !== undefined && AUTH_ERROR_STATUSES.has(status)) {
90
+ return true;
91
+ }
92
+ const message = err instanceof Error ? err.message : String(err);
93
+ const lower = message.toLowerCase();
94
+ return AUTH_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
95
+ }
@@ -297,7 +297,9 @@ function validateToolList(name, field, value) {
297
297
  }
298
298
  catch (err) {
299
299
  const reason = err instanceof Error ? err.message : String(err);
300
- throw new Error(`Server "${name}" has invalid "${field}" pattern "${entry}": ${reason}`);
300
+ throw new Error(`Server "${name}" has invalid "${field}" pattern "${entry}": ${reason}`, {
301
+ cause: err,
302
+ });
301
303
  }
302
304
  patterns.push(entry);
303
305
  }
@@ -86,7 +86,7 @@ export async function discoverSingleServer(server, logger, getAuthProvider) {
86
86
  type: server.type,
87
87
  error: message,
88
88
  });
89
- throw new Error(`Failed to connect to server "${server.name}": ${message}`);
89
+ throw new Error(`Failed to connect to server "${server.name}": ${message}`, { cause: err });
90
90
  }
91
91
  finally {
92
92
  await client.close().catch(() => { });
@@ -95,8 +95,7 @@ function restartGuidance(isStdio) {
95
95
  : 'If the issue persists, restart the MCP server in your coding agent.';
96
96
  return (restartClause +
97
97
  ' If you have already fixed the issue, restart the MCP server in your ' +
98
- 'coding agent (e.g. restart Claude Code, opencode, or Codex) so it ' +
99
- 're-initializes the connection.');
98
+ 'coding agent so it re-initializes the connection.');
100
99
  }
101
100
  function extractMessage(err) {
102
101
  if (err instanceof Error) {
@@ -9,5 +9,5 @@ export { saveToolCache } from './tool-cache.js';
9
9
  export { OAuthCredentialManager } from './oauth.js';
10
10
  export { computeAuthStatus, persistAuthRequirements } from './auth-status.js';
11
11
  export { discoverAuth } from './oauth-discovery.js';
12
- export { GuidedAuthError } from './auth-errors.js';
12
+ export { GuidedAuthError, isAuthError } from './auth-errors.js';
13
13
  export { buildGuidedError } from './guided-error.js';
@@ -1,5 +1,5 @@
1
1
  import { updateServerInCatalog } from './catalog.js';
2
- import { buildGuidedError, GuidedAuthError } from './index.js';
2
+ import { buildGuidedError, isAuthError } from './index.js';
3
3
  /**
4
4
  * Set of error message substrings that indicate a recoverable
5
5
  * network/transport failure (as opposed to a tool-level or protocol
@@ -30,7 +30,7 @@ const RECOVERABLE_PATTERNS = [
30
30
  * @returns True when a reconnect + retry might succeed.
31
31
  */
32
32
  export function isRecoverable(err) {
33
- if (err instanceof GuidedAuthError) {
33
+ if (isAuthError(err)) {
34
34
  return true;
35
35
  }
36
36
  const message = err instanceof Error ? err.message : String(err);
@@ -154,7 +154,7 @@ async function invokeToolWithRetry(conn, tool, args, server, serverConfig, selec
154
154
  // server is reachable, so 'unavailable' would be misleading —
155
155
  // surface auth errors as 'authentication required', re-throw
156
156
  // the rest as-is so the caller sees the real downstream error.
157
- if (retryErr instanceof GuidedAuthError) {
157
+ if (isAuthError(retryErr)) {
158
158
  throw buildGuidedError(serverConfig, retryErr, 'unauthorized', true);
159
159
  }
160
160
  throw retryErr;
@@ -162,7 +162,7 @@ async function invokeToolWithRetry(conn, tool, args, server, serverConfig, selec
162
162
  // Reconnect itself failed: the server is down or requires auth.
163
163
  // doReconnect has already transitioned the connection's status
164
164
  // + cooldown so subsequent calls back off.
165
- const status = retryErr instanceof GuidedAuthError ? 'unauthorized' : 'unavailable';
165
+ const status = isAuthError(retryErr) ? 'unauthorized' : 'unavailable';
166
166
  throw buildGuidedError(serverConfig, retryErr, status, true);
167
167
  }
168
168
  }
@@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { createTransport, listToolsOrEmpty } from './discovery.js';
3
3
  import { OAuthCredentialManager } from './oauth.js';
4
4
  import { saveToolCache, loadToolCache } from './tool-cache.js';
5
- import { GuidedAuthError } from './index.js';
5
+ import { isAuthError } from './index.js';
6
6
  /**
7
7
  * 30-second cooldown after a failed reconnect attempt. Subsequent
8
8
  * `invokeWithRecovery` calls within this window return the cached
@@ -122,7 +122,7 @@ export class ServerConnection {
122
122
  cause: err,
123
123
  });
124
124
  }
125
- const status = err instanceof GuidedAuthError ? 'unauthorized' : 'unavailable';
125
+ const status = isAuthError(err) ? 'unauthorized' : 'unavailable';
126
126
  this._status = status;
127
127
  this.logger.warn(`Server "${this.server.name}" failed (${status}) — using ${cachedTools.length} cached tools`, { server: this.server.name, type: this.server.type, error: message, status });
128
128
  return {
@@ -202,7 +202,7 @@ export class ServerConnection {
202
202
  // backoff. Auth failures are classified as 'unauthorized' so
203
203
  // guided errors point at `login` instead of "connection failed".
204
204
  this._lastError = err instanceof Error ? err.message : String(err);
205
- this._status = err instanceof GuidedAuthError ? 'unauthorized' : 'unavailable';
205
+ this._status = isAuthError(err) ? 'unauthorized' : 'unavailable';
206
206
  throw err;
207
207
  }
208
208
  }
@@ -16,6 +16,6 @@ export function validateGlobPattern(pattern) {
16
16
  }
17
17
  catch (err) {
18
18
  const reason = err instanceof Error ? err.message : String(err);
19
- throw new Error(`Invalid glob pattern "${pattern}": ${reason}`);
19
+ throw new Error(`Invalid glob pattern "${pattern}": ${reason}`, { cause: err });
20
20
  }
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-compress-router",
3
- "version": "1.5.1",
3
+ "version": "1.5.4",
4
4
  "description": "Compress all connected MCP servers into a single router MCP to save tokens",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,17 +25,17 @@
25
25
  "url": "https://github.com/ameshkov/mcp-compress-router/issues"
26
26
  },
27
27
  "scripts": {
28
- "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
29
- "typecheck": "tsc && tsc --project tsconfig.test.json",
28
+ "build": "tsc -p tsconfig.app.json && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
29
+ "typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.test.json",
30
30
  "test": "vitest run",
31
31
  "test:watch": "vitest",
32
32
  "clean": "rm -rf node_modules build",
33
33
  "format:check": "prettier --check --no-error-on-unmatched-pattern --cache . && markdownlint-cli2 .",
34
34
  "format:fix": "prettier --write --no-error-on-unmatched-pattern --cache . && markdownlint-cli2 --fix .",
35
- "lint": "eslint src && pnpm knip",
36
- "lint:fix": "eslint src --fix",
35
+ "lint": "oxlint src && pnpm knip",
36
+ "lint:fix": "oxlint src --fix",
37
37
  "knip": "knip --production",
38
- "check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test",
38
+ "check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run build && pnpm run test",
39
39
  "prepare": "husky || true"
40
40
  },
41
41
  "files": [
@@ -51,25 +51,23 @@
51
51
  ],
52
52
  "author": "Andrey Meshkov <am@adguard.com>",
53
53
  "devDependencies": {
54
- "@eslint/js": "9.16.0",
55
- "@types/node": "24.13.2",
54
+ "@types/node": "26.1.2",
56
55
  "@types/picomatch": "4.0.3",
57
- "eslint": "9.16.0",
58
56
  "husky": "9.1.7",
59
- "knip": "6.6.2",
60
- "markdownlint-cli2": "0.22.0",
61
- "prettier": "3.7.4",
62
- "tsx": "4.20.3",
63
- "typescript": "5.9.3",
64
- "typescript-eslint": "8.57.0",
65
- "vitest": "3.2.4"
57
+ "knip": "6.29.0",
58
+ "markdownlint-cli2": "0.23.2",
59
+ "oxlint": "1.76.0",
60
+ "prettier": "3.9.6",
61
+ "tsx": "4.23.1",
62
+ "typescript": "7.0.2",
63
+ "vitest": "4.1.10"
66
64
  },
67
65
  "dependencies": {
68
- "@modelcontextprotocol/sdk": "1.24.3",
69
- "commander": "14.0.2",
66
+ "@modelcontextprotocol/sdk": "1.30.0",
67
+ "commander": "15.0.0",
70
68
  "dotenv": "17.4.2",
71
69
  "jsonc-parser": "3.3.1",
72
- "picomatch": "4.0.4",
73
- "zod": "3.25.76"
70
+ "picomatch": "4.0.5",
71
+ "zod": "4.4.3"
74
72
  }
75
73
  }