mcp-compress-router 1.3.1 → 1.5.0

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
@@ -25,6 +25,7 @@
25
25
  - [Adding Downstream Servers](#adding-downstream-servers)
26
26
  - [Per-Server Enable/Disable](#per-server-enabledisable)
27
27
  - [Per-Server Tool Selection](#per-server-tool-selection)
28
+ - [Compression Levels](#compression-levels)
28
29
  - [Inspecting Tools](#inspecting-tools)
29
30
  - [OAuth](#oauth)
30
31
  - [Redirect URL](#redirect-url)
@@ -38,6 +39,7 @@
38
39
  - [Codex](#codex)
39
40
  - [GitHub Copilot](#github-copilot)
40
41
  - [How It Works](#how-it-works)
42
+ - [Aknowledgements](#acknowledgements)
41
43
 
42
44
  ## The Problem
43
45
 
@@ -96,17 +98,17 @@ The router is published on npm as
96
98
  You do not need to install it — just run it with `npx`:
97
99
 
98
100
  ```bash
99
- npx mcp-compress-router add github -- npx -y @modelcontextprotocol/server-github
101
+ npx mcp-compress-router@latest add playwright -- npx -y @playwright/mcp
100
102
  ```
101
103
 
102
- This registers a downstream MCP server named `github` and writes it to
103
- your [config file](#config-file-location). Repeat for every MCP server you
104
- want to compress.
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.
105
107
 
106
108
  Then point your [coding agent](#connecting-coding-agents) at the router:
107
109
 
108
110
  ```bash
109
- npx mcp-compress-router
111
+ npx mcp-compress-router@latest
110
112
  ```
111
113
 
112
114
  When started without a subcommand, the router runs the MCP server over
@@ -143,8 +145,9 @@ startup, so you can keep secrets out of the config (see
143
145
 
144
146
  > **Note on `-c` and credential storage:** when you override the config
145
147
  > path with `-c /some/dir/mcp.json`, both `credentials.json` (OAuth
146
- > tokens) and `mcp.json` live in `/some/dir/` — i.e. next to the config
147
- > file you specified. The `.env` file, however, is loaded from the
148
+ > tokens) and `tools-cache.json` (cached tool schemas) will be stored
149
+ > in that directory — i.e. next to the config file you specified. The
150
+ > `.env` file, however, is loaded from the
148
151
  > [configuration directory](#config-file-location) resolved by
149
152
  > `MCP_COMPRESS_ROUTER_HOME` or the platform default, *not* from beside
150
153
  > the explicit `-c` path. To co-locate `.env` with a custom config, set
@@ -164,11 +167,11 @@ far better than a bare name.
164
167
  **stdio server** (a local process):
165
168
 
166
169
  ```bash
167
- npx mcp-compress-router add github --description "GitHub API tools" \
170
+ npx mcp-compress-router@latest add github --description "GitHub API tools" \
168
171
  -- npx -y @modelcontextprotocol/server-github
169
172
 
170
173
  # With environment variables
171
- npx mcp-compress-router add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
174
+ npx mcp-compress-router@latest add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
172
175
  --description "GitHub API tools" \
173
176
  -- npx -y @modelcontextprotocol/server-github
174
177
  ```
@@ -177,10 +180,10 @@ npx mcp-compress-router add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
177
180
  URL):
178
181
 
179
182
  ```bash
180
- npx mcp-compress-router add my-http https://localhost:3100/mcp
183
+ npx mcp-compress-router@latest add my-http https://localhost:3100/mcp
181
184
 
182
185
  # With a custom header
183
- npx mcp-compress-router add my-http \
186
+ npx mcp-compress-router@latest add my-http \
184
187
  --header "Authorization: Bearer mytoken" \
185
188
  https://localhost:3100/mcp
186
189
  ```
@@ -213,9 +216,9 @@ supported. CLI commands write plain `.json`; hand-edited files may use
213
216
  Other management commands:
214
217
 
215
218
  ```bash
216
- npx mcp-compress-router list # list all servers + auth status
217
- npx mcp-compress-router get my-http # show one server's config
218
- npx mcp-compress-router remove my-http # remove a server
219
+ npx mcp-compress-router@latest list # list all servers + auth status
220
+ npx mcp-compress-router@latest get my-http # show one server's config
221
+ npx mcp-compress-router@latest remove my-http # remove a server
219
222
  ```
220
223
 
221
224
  ### Per-Server Enable/Disable
@@ -230,14 +233,14 @@ enabled, keeping `mcp.json` clean and fully backward compatible.
230
233
  Toggle it from the CLI without touching the rest of the config:
231
234
 
232
235
  ```bash
233
- npx mcp-compress-router disable github # writes "enabled": false
234
- npx mcp-compress-router enable github # removes the field
236
+ npx mcp-compress-router@latest disable github # writes "enabled": false
237
+ npx mcp-compress-router@latest enable github # removes the field
235
238
  ```
236
239
 
237
240
  You can also set it at creation time:
238
241
 
239
242
  ```bash
240
- npx mcp-compress-router add archive --disabled -- npx -y server-archive
243
+ npx mcp-compress-router@latest add archive --disabled -- npx -y server-archive
241
244
  ```
242
245
 
243
246
  ### Per-Server Tool Selection
@@ -274,12 +277,67 @@ hard error at startup. Set filters at creation time with repeatable
274
277
  flags:
275
278
 
276
279
  ```bash
277
- npx mcp-compress-router add github \
280
+ npx mcp-compress-router@latest add github \
278
281
  --allowed-tools list_issues \
279
282
  --allowed-tools get_pull_request \
280
283
  -- npx -y server-github
281
284
  ```
282
285
 
286
+ ### Compression Levels
287
+
288
+ Each server's tools are listed in the `get_tool_schema` description at a
289
+ configurable `compressionLevel`. The level trades catalog compactness
290
+ for routing detail: lower levels give the LLM more information up front
291
+ (fewer `get_tool_schema` round-trips), while higher levels minimize the
292
+ per-request token overhead. The full JSON parameter schema is always
293
+ available via `get_tool_schema` regardless of the level — only the
294
+ catalog *listing* changes.
295
+
296
+ Four levels are supported, from most to least compact:
297
+
298
+ | Level | Tool listing format | Description shown? |
299
+ | --- | --- | --- |
300
+ | `max` | `toolA, toolB, toolC` (comma-separated, single line) | No |
301
+ | `high` (default) | `toolName(arg1, arg2)` (one per line) | No |
302
+ | `medium` | `toolName(arg1, arg2): first sentence...` (one per line) | Snippet |
303
+ | `low` | `<tool>toolName(arg1, arg2): full description</tool>` (one per line) | Full |
304
+
305
+ Argument names are extracted from each tool's `inputSchema.properties`
306
+ keys in definition order. When a tool has no description, the `medium`
307
+ and `low` listings omit the description portion and show just the
308
+ signature.
309
+
310
+ Omitting `compressionLevel` (the default) is equivalent to `high`. Set
311
+ it per server in `mcp.json`:
312
+
313
+ ```jsonc
314
+ "github": {
315
+ "type": "stdio",
316
+ "command": "npx",
317
+ "args": ["-y", "@modelcontextprotocol/server-github"],
318
+ "compressionLevel": "medium"
319
+ }
320
+ ```
321
+
322
+ Or set it at creation time with the `--compression-level` flag:
323
+
324
+ ```bash
325
+ npx mcp-compress-router@latest add github \
326
+ --compression-level medium \
327
+ -- npx -y @modelcontextprotocol/server-github
328
+ ```
329
+
330
+ A good rule of thumb:
331
+
332
+ - Use `max` for servers whose tool names are self-describing and you
333
+ want the smallest possible catalog.
334
+ - Use `high` (the default) for most servers — argument names are
335
+ usually enough for the LLM to pick the right tool.
336
+ - Use `medium` when tool names alone are ambiguous and a one-line
337
+ hint helps disambiguate.
338
+ - Use `low` sparingly — only when full descriptions must be visible
339
+ without a `get_tool_schema` call, since it costs the most tokens.
340
+
283
341
  ### Inspecting Tools
284
342
 
285
343
  To see exactly which tools a server advertises — and which are
@@ -287,7 +345,7 @@ To see exactly which tools a server advertises — and which are
287
345
  it live without starting the full router:
288
346
 
289
347
  ```bash
290
- npx mcp-compress-router tools github
348
+ npx mcp-compress-router@latest tools github
291
349
  ```
292
350
 
293
351
  This works regardless of the server's `enabled` state (inspecting a
@@ -304,14 +362,16 @@ flow automatically if OAuth is advertised. You can also trigger it
304
362
  manually:
305
363
 
306
364
  ```bash
307
- npx mcp-compress-router login my-http
365
+ npx mcp-compress-router@latest login my-http
308
366
  ```
309
367
 
310
368
  This opens your browser to complete the authorization-code flow. Tokens
311
369
  are stored in a separate `credentials.json` in the same directory as
312
370
  `mcp.json` (with `0600` permissions on Unix), so you can safely
313
- share or version-control `mcp.json` without exposing tokens. Add
314
- `credentials.json` to your `.gitignore`.
371
+ share or version-control `mcp.json` without exposing tokens. Cached
372
+ tool schemas are stored in `tools-cache.json` in the same directory.
373
+ Add both `credentials.json` and `tools-cache.json` to your
374
+ `.gitignore`.
315
375
 
316
376
  By default the router uses
317
377
  [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591).
@@ -354,7 +414,7 @@ ignore the port on `localhost`. If your provider demands a redirect URI
354
414
  with an **exact port**, pin it with `--port`:
355
415
 
356
416
  ```bash
357
- npx mcp-compress-router login my-http --port 8765
417
+ npx mcp-compress-router@latest login my-http --port 8765
358
418
  ```
359
419
 
360
420
  This binds the callback server to `8765`, so the redirect URI becomes
@@ -374,7 +434,7 @@ flag each time:
374
434
  `--port` overrides `oauth.callbackPort` for a single run. Pass `--port 0`
375
435
  to force an OS-assigned port even when `oauth.callbackPort` is set.
376
436
 
377
- #### GitHub MCP (special case)
437
+ #### GitHub MCP with OAuth (special case)
378
438
 
379
439
  The official GitHub MCP server at
380
440
  `https://api.githubcopilot.com/mcp` advertises OAuth but does **not**
@@ -392,7 +452,7 @@ organizations you want the MCP to access.
392
452
  3. **Add the GitHub MCP server by URL.**
393
453
 
394
454
  ```bash
395
- npx mcp-compress-router add github https://api.githubcopilot.com/mcp
455
+ npx mcp-compress-router@latest add github https://api.githubcopilot.com/mcp
396
456
  ```
397
457
 
398
458
  4. **Set `oauth` credentials in `mcp.json`.**
@@ -419,7 +479,7 @@ organizations you want the MCP to access.
419
479
  5. **Run the login command.**
420
480
 
421
481
  ```bash
422
- npx mcp-compress-router login github
482
+ npx mcp-compress-router@latest login github
423
483
  ```
424
484
 
425
485
  Your browser opens to authorize. After you approve, tokens are stored
@@ -430,7 +490,7 @@ organizations you want the MCP to access.
430
490
  > login will succeed, and its client secret is generated under *General*
431
491
  > → *Generate a new client secret*.
432
492
 
433
- #### Figma MCP (special case)
493
+ #### Figma MCP with OAuth (special case)
434
494
 
435
495
  The official Figma MCP server at `https://mcp.figma.com/mcp` does **not**
436
496
  support Dynamic Client Registration through the standard MCP flow.
@@ -480,7 +540,7 @@ pin it with `oauth.callbackPort`.
480
540
  3. **Add the Figma MCP server by URL.**
481
541
 
482
542
  ```bash
483
- npx mcp-compress-router add --transport http figma https://mcp.figma.com/mcp
543
+ npx mcp-compress-router@latest add --transport http figma https://mcp.figma.com/mcp
484
544
  ```
485
545
 
486
546
  4. **Set `oauth` credentials in `mcp.json`.**
@@ -507,7 +567,7 @@ pin it with `oauth.callbackPort`.
507
567
  5. **Run the login command.**
508
568
 
509
569
  ```bash
510
- npx mcp-compress-router login figma
570
+ npx mcp-compress-router@latest login figma
511
571
  ```
512
572
 
513
573
  Your browser opens to authorize. After you approve, tokens are stored
@@ -516,7 +576,7 @@ pin it with `oauth.callbackPort`.
516
576
  Other OAuth commands:
517
577
 
518
578
  ```bash
519
- npx mcp-compress-router logout my-http # remove stored credentials
579
+ npx mcp-compress-router@latest logout my-http # remove stored credentials
520
580
  ```
521
581
 
522
582
  For headless or CI environments, override the browser with the
@@ -525,7 +585,7 @@ URL is appended as a single final argument (no shell):
525
585
 
526
586
  ```bash
527
587
  MCP_COMPRESS_ROUTER_BROWSER="node /path/to/headless-browser.js" \
528
- npx mcp-compress-router login my-http
588
+ npx mcp-compress-router@latest login my-http
529
589
  ```
530
590
 
531
591
  The default login timeout is 120 seconds; override it with
@@ -538,7 +598,7 @@ token instead of OAuth, use the `headers` field. You can set it via the
538
598
  CLI or directly in `mcp.json`:
539
599
 
540
600
  ```bash
541
- npx mcp-compress-router add my-http \
601
+ npx mcp-compress-router@latest add my-http \
542
602
  --header "Authorization: Bearer mytoken" \
543
603
  --header "X-Custom: value" \
544
604
  https://example.com/mcp
@@ -584,68 +644,34 @@ Shell environment variables always take precedence over `.env` values.
584
644
 
585
645
  Once your downstream servers are configured, connect your agent to the
586
646
  router the same way you would connect any other MCP server — by
587
- pointing it at `npx mcp-compress-router`. The examples below assume the
647
+ pointing it at `npx mcp-compress-router@latest`. The examples below assume the
588
648
  default [config location](#config-file-location); pass `-c <path>` if
589
649
  you use a custom one.
590
650
 
591
651
  ### Opencode
592
652
 
593
- Add the router to your `opencode.json` under `mcp`:
594
-
595
- ```json
596
- {
597
- "$schema": "https://opencode.ai/config.json",
598
- "mcp": {
599
- "mcp-compress-router": {
600
- "type": "local",
601
- "command": ["npx", "-y", "mcp-compress-router"],
602
- "enabled": true
603
- }
604
- }
605
- }
653
+ ```sh
654
+ opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
606
655
  ```
607
656
 
608
657
  ### Claude Code
609
658
 
610
- Add this to a project-level `.mcp.json` in your workspace root, or to
611
- your user-level config (applies to every project):
612
-
613
- - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
614
- - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
615
- - **Linux:** `~/.config/Claude/claude_desktop_config.json`
616
-
617
- ```json
618
- {
619
- "mcpServers": {
620
- "mcp-compress-router": {
621
- "command": "npx",
622
- "args": ["-y", "mcp-compress-router"]
623
- }
624
- }
625
- }
659
+ ```sh
660
+ claude mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
626
661
  ```
627
662
 
628
663
  ### Codex
629
664
 
630
- Add a `[mcp_servers.mcp-compress-router]` table to your Codex config (note
631
- the snake_case key). The config path is `~/.codex/config.toml` on
632
- macOS/Linux, or `%USERPROFILE%\.codex\config.toml` on Windows; you can
633
- also scope it to a single project via `.codex/config.toml` in trusted
634
- projects.
635
-
636
- ```toml
637
- [mcp_servers.mcp-compress-router]
638
- command = "npx"
639
- args = ["-y", "mcp-compress-router"]
640
- enabled = true
665
+ ```sh
666
+ codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
641
667
  ```
642
668
 
643
- ### GitHub Copilot
669
+ ### GitHub Copilot (VS Code)
644
670
 
645
671
  Add this to `.vscode/mcp.json` in your workspace (project-level, applies
646
672
  only to that workspace), or to your **user-level** MCP settings which
647
- apply across every workspace: open the Command Palette →
648
- `Preferences: Open User Settings (JSON)` and add the same `servers`
673
+ apply across every workspace: open the Command Palette (`Cmd+Shift+P`) →
674
+ `MCP: Open User Configuration` and add the same `servers`
649
675
  block under the `mcp` key. Project-level and user-level entries are
650
676
  merged, with project-level taking precedence.
651
677
 
@@ -654,7 +680,7 @@ merged, with project-level taking precedence.
654
680
  "servers": {
655
681
  "mcp-compress-router": {
656
682
  "command": "npx",
657
- "args": ["-y", "mcp-compress-router"]
683
+ "args": ["-y", "mcp-compress-router@latest"]
658
684
  }
659
685
  }
660
686
  }
@@ -684,3 +710,11 @@ token catalog, regardless of how many downstream servers you have.
684
710
 
685
711
  For the full configuration and environment variable reference, see
686
712
  [configuration.md](docs/configuration.md).
713
+
714
+ ## Acknowledgements
715
+
716
+ - [mcp2cli](https://github.com/knowsuchagency/mcp2cli) — a very similar
717
+ idea of how MCP can be compressed, but to a CLI.
718
+ - [mcp-compressor](https://github.com/atlassian-labs/mcp-compressor) —
719
+ also a very similar idea; the "two tools" approach was borrowed from
720
+ this project, though it only compresses a single MCP server.
@@ -1,5 +1,5 @@
1
1
  import { ensureConfigDir, readConfigFile, writeConfigFile, readCredentials, writeCredentials, } from './config-io.js';
2
- import { validateGlobPattern } from '../utils/index.js';
2
+ import { isCompressionLevel, validateGlobPattern, VALID_COMPRESSION_LEVELS, } from '../utils/index.js';
3
3
  /**
4
4
  * Validates every glob pattern in an optional tool list, throwing with
5
5
  * the field name and offending pattern on the first invalid entry.
@@ -61,6 +61,9 @@ function buildServerEntry(opts) {
61
61
  if (opts.disabledTools && opts.disabledTools.length > 0) {
62
62
  entry.disabledTools = opts.disabledTools;
63
63
  }
64
+ if (opts.compressionLevel) {
65
+ entry.compressionLevel = opts.compressionLevel;
66
+ }
64
67
  // A fixed callback port only applies to HTTP servers (OAuth). Persist
65
68
  // it on the `oauth` block so `login` reuses the same redirect URI.
66
69
  if (opts.port !== undefined) {
@@ -92,6 +95,10 @@ export async function handleAdd(configPath, opts) {
92
95
  }
93
96
  validateToolListPatterns('allowedTools', opts.allowedTools);
94
97
  validateToolListPatterns('disabledTools', opts.disabledTools);
98
+ if (opts.compressionLevel !== undefined && !isCompressionLevel(opts.compressionLevel)) {
99
+ throw new Error(`Invalid "--compression-level" value "${opts.compressionLevel}": ` +
100
+ `must be one of ${VALID_COMPRESSION_LEVELS.join(', ')}.`);
101
+ }
95
102
  await ensureConfigDir(configPath);
96
103
  const servers = await readConfigFile(configPath);
97
104
  if (opts.name in servers) {
@@ -24,6 +24,12 @@ export async function handleGet(configPath, name) {
24
24
  if (entry.description) {
25
25
  lines.push(`Description: ${entry.description}`);
26
26
  }
27
+ if (entry.compressionLevel) {
28
+ lines.push(`compressionLevel: ${entry.compressionLevel}`);
29
+ }
30
+ else {
31
+ lines.push('compressionLevel: high (default)');
32
+ }
27
33
  if (entry.command) {
28
34
  lines.push(`Command: ${entry.command}`);
29
35
  }
@@ -55,6 +55,16 @@ function summarizeTools(server) {
55
55
  }
56
56
  return 'all';
57
57
  }
58
+ /**
59
+ * Renders the `Compression` cell: the explicit `compressionLevel`, or
60
+ * `high` when the field is absent (the default per PRD §"Solution").
61
+ *
62
+ * @param server - Typed downstream server config.
63
+ * @returns The resolved compression level string.
64
+ */
65
+ function summarizeCompression(server) {
66
+ return server.compressionLevel ?? 'high';
67
+ }
58
68
  /**
59
69
  * Renders the list header and server rows as a fixed-width table. The
60
70
  * final (Auth) column is left unpadded so lines never carry trailing
@@ -74,13 +84,14 @@ function formatList(configPath, rows) {
74
84
  const commandWidth = Math.max('CommandOrUrl'.length, ...rows.map((r) => r.commandOrUrl.length));
75
85
  const enabledWidth = Math.max('Enabled'.length, ...rows.map((r) => r.enabled.length));
76
86
  const toolsWidth = Math.max('Tools'.length, ...rows.map((r) => r.tools.length));
87
+ const compressionWidth = Math.max('Compression'.length, ...rows.map((r) => r.compression.length));
77
88
  const pad = (val, width) => val.padEnd(width);
78
- const columns = (name, type, command, enabled, tools, auth) => `${pad(name, nameWidth)} ${pad(type, typeWidth)} ${pad(command, commandWidth)} ${pad(enabled, enabledWidth)} ${pad(tools, toolsWidth)} ${auth}`;
89
+ const columns = (name, type, command, enabled, tools, compression, auth) => `${pad(name, nameWidth)} ${pad(type, typeWidth)} ${pad(command, commandWidth)} ${pad(enabled, enabledWidth)} ${pad(tools, toolsWidth)} ${pad(compression, compressionWidth)} ${auth}`;
79
90
  return [
80
91
  header,
81
92
  '',
82
- columns('Name', 'Type', 'CommandOrUrl', 'Enabled', 'Tools', 'Auth'),
83
- ...rows.map((r) => columns(r.name, r.type, r.commandOrUrl, r.enabled, r.tools, r.auth)),
93
+ columns('Name', 'Type', 'CommandOrUrl', 'Enabled', 'Tools', 'Compression', 'Auth'),
94
+ ...rows.map((r) => columns(r.name, r.type, r.commandOrUrl, r.enabled, r.tools, r.compression, r.auth)),
84
95
  ].join('\n');
85
96
  }
86
97
  /**
@@ -107,6 +118,7 @@ export async function handleList(configPath) {
107
118
  enabled: entry.enabled,
108
119
  allowedTools: entry.allowedTools,
109
120
  disabledTools: entry.disabledTools,
121
+ compressionLevel: entry.compressionLevel,
110
122
  };
111
123
  return {
112
124
  name,
@@ -114,6 +126,7 @@ export async function handleList(configPath) {
114
126
  commandOrUrl: buildCommandOrUrl(typed),
115
127
  enabled: summarizeEnabled(typed),
116
128
  tools: summarizeTools(typed),
129
+ compression: summarizeCompression(typed),
117
130
  auth: computeAuthStatus(typed, credentials[name]),
118
131
  };
119
132
  });
@@ -1,6 +1,7 @@
1
+ import { Logger } from '../utils/index.js';
1
2
  import { ensureConfigDir, readConfigFile } from './config-io.js';
2
3
  import { loadConfig } from '../services/config.js';
3
- import { discoverAuth } from '../services/index.js';
4
+ import { discoverAuth, discoverSingleServer, saveToolCache } from '../services/index.js';
4
5
  /**
5
6
  * Validates that a server exists in config and is eligible for OAuth login.
6
7
  *
@@ -260,5 +261,16 @@ export async function handleLogin(configPath, name, portOverride) {
260
261
  redirectUri: realRedirectUrl,
261
262
  });
262
263
  await mgr.saveTokens(tokens);
264
+ // Best-effort: discover tools and save to cache so the next router
265
+ // startup (or self-recovery) has an up-to-date cache.
266
+ try {
267
+ const logger = new Logger('error');
268
+ const discovered = await discoverSingleServer(targetServer, logger, () => mgr);
269
+ await saveToolCache(configPath, name, discovered.tools);
270
+ }
271
+ catch {
272
+ // Best-effort — tokens are saved, cache will refresh on next
273
+ // router startup.
274
+ }
263
275
  return `Successfully authenticated server "${name}". Tokens stored in credentials.json.`;
264
276
  }
@@ -84,6 +84,7 @@ function buildAddOptions(name, commandOrUrl, rest, options) {
84
84
  allowedTools: options.allowedTools && options.allowedTools.length > 0 ? options.allowedTools : undefined,
85
85
  disabledTools: options.disabledTools && options.disabledTools.length > 0 ? options.disabledTools : undefined,
86
86
  port: options.port,
87
+ compressionLevel: options.compressionLevel || undefined,
87
88
  };
88
89
  }
89
90
  function registerAddCommand(program) {
@@ -99,6 +100,7 @@ function registerAddCommand(program) {
99
100
  .option('--disabled', 'mark the server as disabled (writes "enabled": false)')
100
101
  .option('--allowed-tools <pattern>', 'glob pattern allowlisting tool names (repeatable)', collectStringArray, [])
101
102
  .option('--disabled-tools <pattern>', 'glob pattern denylisting tool names (repeatable)', collectStringArray, [])
103
+ .option('--compression-level <level>', 'tool listing compression level (max, high, medium, low; default high)')
102
104
  .option('-p, --port <number>', 'fixed local OAuth callback port (HTTP only; written to oauth.callbackPort)', parsePort)
103
105
  .action(guardedAction(async (name, commandOrUrl, rest, options) => {
104
106
  const configPath = await resolveConfigPath(options.config);
@@ -1,31 +1,50 @@
1
- import { resolveConfigPath, invokeDownstreamTool, OAuthCredentialManager, persistAuthRequirements, loadConfig, connectAndDiscover, buildCatalog, } from '../services/index.js';
1
+ import { resolveConfigPath, persistAuthRequirements, loadConfig, ServerConnection, invokeWithRecovery, buildCatalog, } from '../services/index.js';
2
2
  import { Logger } from '../utils/index.js';
3
3
  import { createGetToolSchemaHandler, buildGetToolSchemaDescription, GetToolSchemaInputSchema, createInvokeToolHandler, InvokeToolInputSchema, } from '../tools/index.js';
4
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
6
  /**
7
- * Creates OAuth credential managers for HTTP servers that have stored
8
- * credentials or oauth overrides, so the transport can handle auth.
7
+ * Connects to all enabled downstream servers via ServerConnection.
8
+ * Disabled servers are skipped entirely. On failure, warm-cache
9
+ * servers are degraded; cold-cache servers cause fail-fast.
10
+ *
11
+ * @param servers - Validated downstream server configs.
12
+ * @param configPath - Absolute path to the config file.
13
+ * @param logger - Structured logger.
14
+ * @returns Map of server name to ServerConnection and discovered data.
15
+ * @throws When a server cannot connect AND has no tool cache.
9
16
  */
10
- async function buildAuthProviders(resolved, servers) {
11
- const authProviders = new Map();
12
- for (const server of servers) {
13
- if (server.type === 'http' || server.type === 'streamable-http') {
14
- const mgr = new OAuthCredentialManager(resolved, server);
15
- const hasCredentials = (await mgr.tokens()) !== undefined;
16
- const hasOverrides = server.oauth?.clientId !== undefined;
17
- if (hasCredentials || hasOverrides) {
18
- authProviders.set(server.name, mgr);
19
- }
17
+ async function connectAllServers(servers, configPath, logger) {
18
+ const enabledServers = servers.filter((server) => {
19
+ if (server.enabled === false) {
20
+ logger.info(`Skipping disabled server "${server.name}"`, { server: server.name });
21
+ return false;
20
22
  }
23
+ return true;
24
+ });
25
+ const results = await Promise.all(enabledServers.map(async (server) => {
26
+ const conn = new ServerConnection(server, configPath, logger);
27
+ const ds = await conn.connect();
28
+ return { conn, ds };
29
+ }));
30
+ const connections = new Map();
31
+ const discovered = [];
32
+ for (const { conn, ds } of results) {
33
+ connections.set(conn.serverName, conn);
34
+ discovered.push(ds);
21
35
  }
22
- return authProviders;
36
+ return { connections, discovered };
23
37
  }
24
38
  /**
25
39
  * Creates the MCP server, registers router tools, and starts the
26
40
  * stdio transport.
41
+ *
42
+ * @param catalog - The mutable tool catalog.
43
+ * @param connections - Live ServerConnection instances keyed by name.
44
+ * @param selectionByServer - Per-server tool selection for re-filtering.
45
+ * @param logger - Structured logger.
27
46
  */
28
- async function startRouterServer(catalog, clients, logger) {
47
+ async function startRouterServer(catalog, connections, selectionByServer, logger) {
29
48
  const router = new McpServer({
30
49
  name: 'mcp-compress-router',
31
50
  version: '1.0.0',
@@ -35,7 +54,9 @@ async function startRouterServer(catalog, clients, logger) {
35
54
  description: buildGetToolSchemaDescription(catalog),
36
55
  inputSchema: GetToolSchemaInputSchema,
37
56
  }, createGetToolSchemaHandler(catalog, logger));
38
- const invokeFn = (server, tool, args) => invokeDownstreamTool(clients, server, tool, args, logger);
57
+ const invokeFn = async (server, tool, args) => {
58
+ return invokeWithRecovery(server, tool, args, catalog, connections, selectionByServer, logger);
59
+ };
39
60
  router.registerTool('invoke_tool', {
40
61
  title: 'Invoke Tool',
41
62
  description: 'Invoke a specific tool on a connected MCP server. ' +
@@ -66,26 +87,26 @@ export async function runRouter(configPath, verbose) {
66
87
  const servers = await loadConfig(resolved);
67
88
  logger.info('Configuration loaded', { serverCount: servers.length });
68
89
  await persistAuthRequirements(resolved, servers, logger);
69
- const authProviders = await buildAuthProviders(resolved, servers);
70
- const getAuthProvider = (server) => authProviders.get(server.name);
71
90
  logger.info('Connecting to downstream servers', {
72
91
  servers: servers.map((s) => s.name),
73
92
  });
74
- const { servers: discovered, clients } = await connectAndDiscover(servers, logger, getAuthProvider);
93
+ const { connections, discovered } = await connectAllServers(servers, resolved, logger);
75
94
  logger.info('Tools discovered', {
76
95
  servers: discovered.map((d) => ({
77
96
  name: d.name,
78
97
  toolCount: d.tools.length,
79
- tools: d.tools.map((t) => t.name),
98
+ status: d.status,
80
99
  })),
81
100
  });
82
101
  const selectionByServer = new Map();
102
+ const compressionLevelByServer = new Map();
83
103
  for (const server of servers) {
84
104
  selectionByServer.set(server.name, {
85
105
  allowedTools: server.allowedTools,
86
106
  disabledTools: server.disabledTools,
87
107
  });
108
+ compressionLevelByServer.set(server.name, server.compressionLevel);
88
109
  }
89
- const catalog = buildCatalog(discovered, selectionByServer, logger);
90
- await startRouterServer(catalog, clients, logger);
110
+ const catalog = buildCatalog(discovered, selectionByServer, logger, compressionLevelByServer);
111
+ await startRouterServer(catalog, connections, selectionByServer, logger);
91
112
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Tagged error thrown when a downstream server requires OAuth
3
+ * authentication that cannot be completed from the running router
4
+ * process.
5
+ *
6
+ * The MCP SDK calls `OAuthClientProvider.redirectToAuthorization()` on
7
+ * a 401 response. This error is thrown from that method so callers
8
+ * can discriminate auth failures from other errors (network, config,
9
+ * transport) without relying on substring matching of error messages.
10
+ */
11
+ export class GuidedAuthError extends Error {
12
+ /** The downstream server name that requires authentication. */
13
+ serverName;
14
+ /**
15
+ * @param serverName - The name of the server requiring authentication.
16
+ */
17
+ constructor(serverName) {
18
+ super(`Authentication required for server "${serverName}".`);
19
+ this.name = 'GuidedAuthError';
20
+ this.serverName = serverName;
21
+ }
22
+ }