mcp-compress-router 1.3.0 → 1.4.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,10 +25,12 @@
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)
31
32
  - [GitHub MCP (special case)](#github-mcp-special-case)
33
+ - [Figma MCP (special case)](#figma-mcp-special-case)
32
34
  - [Custom Headers](#custom-headers)
33
35
  - [Secrets and Variable Expansion](#secrets-and-variable-expansion)
34
36
  - [Connecting Coding Agents](#connecting-coding-agents)
@@ -37,6 +39,7 @@
37
39
  - [Codex](#codex)
38
40
  - [GitHub Copilot](#github-copilot)
39
41
  - [How It Works](#how-it-works)
42
+ - [Aknowledgements](#acknowledgements)
40
43
 
41
44
  ## The Problem
42
45
 
@@ -95,17 +98,17 @@ The router is published on npm as
95
98
  You do not need to install it — just run it with `npx`:
96
99
 
97
100
  ```bash
98
- npx mcp-compress-router add github -- npx -y @modelcontextprotocol/server-github
101
+ npx mcp-compress-router@latest add playwright -- npx -y @playwright/mcp
99
102
  ```
100
103
 
101
- This registers a downstream MCP server named `github` and writes it to
102
- your [config file](#config-file-location). Repeat for every MCP server you
103
- 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.
104
107
 
105
108
  Then point your [coding agent](#connecting-coding-agents) at the router:
106
109
 
107
110
  ```bash
108
- npx mcp-compress-router
111
+ npx mcp-compress-router@latest
109
112
  ```
110
113
 
111
114
  When started without a subcommand, the router runs the MCP server over
@@ -163,11 +166,11 @@ far better than a bare name.
163
166
  **stdio server** (a local process):
164
167
 
165
168
  ```bash
166
- npx mcp-compress-router add github --description "GitHub API tools" \
169
+ npx mcp-compress-router@latest add github --description "GitHub API tools" \
167
170
  -- npx -y @modelcontextprotocol/server-github
168
171
 
169
172
  # With environment variables
170
- npx mcp-compress-router add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
173
+ npx mcp-compress-router@latest add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
171
174
  --description "GitHub API tools" \
172
175
  -- npx -y @modelcontextprotocol/server-github
173
176
  ```
@@ -176,10 +179,10 @@ npx mcp-compress-router add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
176
179
  URL):
177
180
 
178
181
  ```bash
179
- npx mcp-compress-router add my-http https://localhost:3100/mcp
182
+ npx mcp-compress-router@latest add my-http https://localhost:3100/mcp
180
183
 
181
184
  # With a custom header
182
- npx mcp-compress-router add my-http \
185
+ npx mcp-compress-router@latest add my-http \
183
186
  --header "Authorization: Bearer mytoken" \
184
187
  https://localhost:3100/mcp
185
188
  ```
@@ -212,9 +215,9 @@ supported. CLI commands write plain `.json`; hand-edited files may use
212
215
  Other management commands:
213
216
 
214
217
  ```bash
215
- npx mcp-compress-router list # list all servers + auth status
216
- npx mcp-compress-router get my-http # show one server's config
217
- npx mcp-compress-router remove my-http # remove a server
218
+ npx mcp-compress-router@latest list # list all servers + auth status
219
+ npx mcp-compress-router@latest get my-http # show one server's config
220
+ npx mcp-compress-router@latest remove my-http # remove a server
218
221
  ```
219
222
 
220
223
  ### Per-Server Enable/Disable
@@ -229,14 +232,14 @@ enabled, keeping `mcp.json` clean and fully backward compatible.
229
232
  Toggle it from the CLI without touching the rest of the config:
230
233
 
231
234
  ```bash
232
- npx mcp-compress-router disable github # writes "enabled": false
233
- npx mcp-compress-router enable github # removes the field
235
+ npx mcp-compress-router@latest disable github # writes "enabled": false
236
+ npx mcp-compress-router@latest enable github # removes the field
234
237
  ```
235
238
 
236
239
  You can also set it at creation time:
237
240
 
238
241
  ```bash
239
- npx mcp-compress-router add archive --disabled -- npx -y server-archive
242
+ npx mcp-compress-router@latest add archive --disabled -- npx -y server-archive
240
243
  ```
241
244
 
242
245
  ### Per-Server Tool Selection
@@ -273,12 +276,67 @@ hard error at startup. Set filters at creation time with repeatable
273
276
  flags:
274
277
 
275
278
  ```bash
276
- npx mcp-compress-router add github \
279
+ npx mcp-compress-router@latest add github \
277
280
  --allowed-tools list_issues \
278
281
  --allowed-tools get_pull_request \
279
282
  -- npx -y server-github
280
283
  ```
281
284
 
285
+ ### Compression Levels
286
+
287
+ Each server's tools are listed in the `get_tool_schema` description at a
288
+ configurable `compressionLevel`. The level trades catalog compactness
289
+ for routing detail: lower levels give the LLM more information up front
290
+ (fewer `get_tool_schema` round-trips), while higher levels minimize the
291
+ per-request token overhead. The full JSON parameter schema is always
292
+ available via `get_tool_schema` regardless of the level — only the
293
+ catalog *listing* changes.
294
+
295
+ Four levels are supported, from most to least compact:
296
+
297
+ | Level | Tool listing format | Description shown? |
298
+ | --- | --- | --- |
299
+ | `max` | `toolA, toolB, toolC` (comma-separated, single line) | No |
300
+ | `high` (default) | `toolName(arg1, arg2)` (one per line) | No |
301
+ | `medium` | `toolName(arg1, arg2): first sentence...` (one per line) | Snippet |
302
+ | `low` | `<tool>toolName(arg1, arg2): full description</tool>` (one per line) | Full |
303
+
304
+ Argument names are extracted from each tool's `inputSchema.properties`
305
+ keys in definition order. When a tool has no description, the `medium`
306
+ and `low` listings omit the description portion and show just the
307
+ signature.
308
+
309
+ Omitting `compressionLevel` (the default) is equivalent to `high`. Set
310
+ it per server in `mcp.json`:
311
+
312
+ ```jsonc
313
+ "github": {
314
+ "type": "stdio",
315
+ "command": "npx",
316
+ "args": ["-y", "@modelcontextprotocol/server-github"],
317
+ "compressionLevel": "medium"
318
+ }
319
+ ```
320
+
321
+ Or set it at creation time with the `--compression-level` flag:
322
+
323
+ ```bash
324
+ npx mcp-compress-router@latest add github \
325
+ --compression-level medium \
326
+ -- npx -y @modelcontextprotocol/server-github
327
+ ```
328
+
329
+ A good rule of thumb:
330
+
331
+ - Use `max` for servers whose tool names are self-describing and you
332
+ want the smallest possible catalog.
333
+ - Use `high` (the default) for most servers — argument names are
334
+ usually enough for the LLM to pick the right tool.
335
+ - Use `medium` when tool names alone are ambiguous and a one-line
336
+ hint helps disambiguate.
337
+ - Use `low` sparingly — only when full descriptions must be visible
338
+ without a `get_tool_schema` call, since it costs the most tokens.
339
+
282
340
  ### Inspecting Tools
283
341
 
284
342
  To see exactly which tools a server advertises — and which are
@@ -286,7 +344,7 @@ To see exactly which tools a server advertises — and which are
286
344
  it live without starting the full router:
287
345
 
288
346
  ```bash
289
- npx mcp-compress-router tools github
347
+ npx mcp-compress-router@latest tools github
290
348
  ```
291
349
 
292
350
  This works regardless of the server's `enabled` state (inspecting a
@@ -303,7 +361,7 @@ flow automatically if OAuth is advertised. You can also trigger it
303
361
  manually:
304
362
 
305
363
  ```bash
306
- npx mcp-compress-router login my-http
364
+ npx mcp-compress-router@latest login my-http
307
365
  ```
308
366
 
309
367
  This opens your browser to complete the authorization-code flow. Tokens
@@ -353,7 +411,7 @@ ignore the port on `localhost`. If your provider demands a redirect URI
353
411
  with an **exact port**, pin it with `--port`:
354
412
 
355
413
  ```bash
356
- npx mcp-compress-router login my-http --port 8765
414
+ npx mcp-compress-router@latest login my-http --port 8765
357
415
  ```
358
416
 
359
417
  This binds the callback server to `8765`, so the redirect URI becomes
@@ -373,7 +431,7 @@ flag each time:
373
431
  `--port` overrides `oauth.callbackPort` for a single run. Pass `--port 0`
374
432
  to force an OS-assigned port even when `oauth.callbackPort` is set.
375
433
 
376
- #### GitHub MCP (special case)
434
+ #### GitHub MCP with OAuth (special case)
377
435
 
378
436
  The official GitHub MCP server at
379
437
  `https://api.githubcopilot.com/mcp` advertises OAuth but does **not**
@@ -391,7 +449,7 @@ organizations you want the MCP to access.
391
449
  3. **Add the GitHub MCP server by URL.**
392
450
 
393
451
  ```bash
394
- npx mcp-compress-router add github https://api.githubcopilot.com/mcp
452
+ npx mcp-compress-router@latest add github https://api.githubcopilot.com/mcp
395
453
  ```
396
454
 
397
455
  4. **Set `oauth` credentials in `mcp.json`.**
@@ -418,7 +476,7 @@ organizations you want the MCP to access.
418
476
  5. **Run the login command.**
419
477
 
420
478
  ```bash
421
- npx mcp-compress-router login github
479
+ npx mcp-compress-router@latest login github
422
480
  ```
423
481
 
424
482
  Your browser opens to authorize. After you approve, tokens are stored
@@ -429,10 +487,93 @@ organizations you want the MCP to access.
429
487
  > login will succeed, and its client secret is generated under *General*
430
488
  > → *Generate a new client secret*.
431
489
 
490
+ #### Figma MCP with OAuth (special case)
491
+
492
+ The official Figma MCP server at `https://mcp.figma.com/mcp` does **not**
493
+ support Dynamic Client Registration through the standard MCP flow.
494
+ Instead you register an OAuth client via Figma's REST API using a
495
+ Personal Access Token, then pass the resulting credentials through the
496
+ `oauth` block. Figma also requires the redirect URI to use a **fixed
497
+ port** — the port you register is reused on every `login`, so you must
498
+ pin it with `oauth.callbackPort`.
499
+
500
+ 1. **Create a Figma Personal Access Token.**
501
+ Follow
502
+ <https://developers.figma.com/docs/rest-api/personal-access-tokens/>
503
+ to generate a PAT and export it as `FIGMA_PERSONAL_ACCESS_TOKEN`. It
504
+ is only used to register the MCP client in the next step.
505
+
506
+ 2. **Register the MCP client via Figma's API.**
507
+ The redirect URI must use `127.0.0.1` on a fixed port — **the port
508
+ matters**, it is reused on every `login`. This example uses `19876`:
509
+
510
+ ```bash
511
+ curl -X POST https://api.figma.com/v1/oauth/mcp/register \
512
+ -H "Content-Type: application/json" \
513
+ -H "X-Figma-Token: $FIGMA_PERSONAL_ACCESS_TOKEN" \
514
+ -d '{
515
+ "client_name": "Claude Code (figma)",
516
+ "redirect_uris": ["http://127.0.0.1:19876/mcp-compress-router/oauth-callback"],
517
+ "grant_types": ["authorization_code", "refresh_token"],
518
+ "response_types": ["code"],
519
+ "token_endpoint_auth_method": "none"
520
+ }'
521
+ ```
522
+
523
+ Save the `client_id` and `client_secret` from the response (also note
524
+ the `scope` is `mcp:connect`):
525
+
526
+ ```json
527
+ {
528
+ "client_id": "CLIENTID",
529
+ "client_secret": "CLIENTSECRET",
530
+ "client_name": "Claude Code (figma)",
531
+ "redirect_uris": ["http://127.0.0.1:19876/mcp-compress-router/oauth-callback"],
532
+ "token_endpoint_auth_method": "none",
533
+ "scope": "mcp:connect"
534
+ }
535
+ ```
536
+
537
+ 3. **Add the Figma MCP server by URL.**
538
+
539
+ ```bash
540
+ npx mcp-compress-router@latest add --transport http figma https://mcp.figma.com/mcp
541
+ ```
542
+
543
+ 4. **Set `oauth` credentials in `mcp.json`.**
544
+ Put the client ID and secret from step 2 in the server entry, using
545
+ the `mcp:connect` scope and the **same fixed port** you registered as
546
+ `callbackPort`:
547
+
548
+ ```jsonc
549
+ "figma": {
550
+ "type": "http",
551
+ "url": "https://mcp.figma.com/mcp",
552
+ "oauth": {
553
+ "clientId": "${FIGMA_CLIENT_ID}",
554
+ "clientSecret": "${FIGMA_CLIENT_SECRET}",
555
+ "scope": "mcp:connect",
556
+ "callbackPort": 19876
557
+ }
558
+ }
559
+ ```
560
+
561
+ Put the actual values in your `.env` file (see
562
+ [Secrets and Variable Expansion](#secrets-and-variable-expansion)).
563
+
564
+ 5. **Run the login command.**
565
+
566
+ ```bash
567
+ npx mcp-compress-router@latest login figma
568
+ ```
569
+
570
+ Your browser opens to authorize. After you approve, tokens are stored
571
+ in `credentials.json` and the router can call Figma MCP tools.
572
+
432
573
  Other OAuth commands:
433
574
 
434
575
  ```bash
435
- npx mcp-compress-router logout my-http # remove stored credentials
576
+ npx mcp-compress-router@latest logout my-http # remove stored credentials
436
577
  ```
437
578
 
438
579
  For headless or CI environments, override the browser with the
@@ -441,7 +582,7 @@ URL is appended as a single final argument (no shell):
441
582
 
442
583
  ```bash
443
584
  MCP_COMPRESS_ROUTER_BROWSER="node /path/to/headless-browser.js" \
444
- npx mcp-compress-router login my-http
585
+ npx mcp-compress-router@latest login my-http
445
586
  ```
446
587
 
447
588
  The default login timeout is 120 seconds; override it with
@@ -454,7 +595,7 @@ token instead of OAuth, use the `headers` field. You can set it via the
454
595
  CLI or directly in `mcp.json`:
455
596
 
456
597
  ```bash
457
- npx mcp-compress-router add my-http \
598
+ npx mcp-compress-router@latest add my-http \
458
599
  --header "Authorization: Bearer mytoken" \
459
600
  --header "X-Custom: value" \
460
601
  https://example.com/mcp
@@ -500,77 +641,43 @@ Shell environment variables always take precedence over `.env` values.
500
641
 
501
642
  Once your downstream servers are configured, connect your agent to the
502
643
  router the same way you would connect any other MCP server — by
503
- pointing it at `npx mcp-compress-router`. The examples below assume the
644
+ pointing it at `npx mcp-compress-router@latest`. The examples below assume the
504
645
  default [config location](#config-file-location); pass `-c <path>` if
505
646
  you use a custom one.
506
647
 
507
648
  ### Opencode
508
649
 
509
- Add the router to your `opencode.json` under `mcp`:
510
-
511
- ```json
512
- {
513
- "$schema": "https://opencode.ai/config.json",
514
- "mcp": {
515
- "compress-router": {
516
- "type": "local",
517
- "command": ["npx", "-y", "mcp-compress-router"],
518
- "enabled": true
519
- }
520
- }
521
- }
650
+ ```sh
651
+ opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
522
652
  ```
523
653
 
524
654
  ### Claude Code
525
655
 
526
- Add this to a project-level `.mcp.json` in your workspace root, or to
527
- your user-level config (applies to every project):
528
-
529
- - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
530
- - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
531
- - **Linux:** `~/.config/Claude/claude_desktop_config.json`
532
-
533
- ```json
534
- {
535
- "mcpServers": {
536
- "compress-router": {
537
- "command": "npx",
538
- "args": ["-y", "mcp-compress-router"]
539
- }
540
- }
541
- }
656
+ ```sh
657
+ claude mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
542
658
  ```
543
659
 
544
660
  ### Codex
545
661
 
546
- Add a `[mcp_servers.compress-router]` table to your Codex config (note
547
- the snake_case key). The config path is `~/.codex/config.toml` on
548
- macOS/Linux, or `%USERPROFILE%\.codex\config.toml` on Windows; you can
549
- also scope it to a single project via `.codex/config.toml` in trusted
550
- projects.
551
-
552
- ```toml
553
- [mcp_servers.compress-router]
554
- command = "npx"
555
- args = ["-y", "mcp-compress-router"]
556
- enabled = true
662
+ ```sh
663
+ codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
557
664
  ```
558
665
 
559
- ### GitHub Copilot
666
+ ### GitHub Copilot (VS Code)
560
667
 
561
668
  Add this to `.vscode/mcp.json` in your workspace (project-level, applies
562
669
  only to that workspace), or to your **user-level** MCP settings which
563
- apply across every workspace: open the Command Palette →
564
- `Preferences: Open User Settings (JSON)` and add the same `servers`
670
+ apply across every workspace: open the Command Palette (`Cmd+Shift+P`) →
671
+ `MCP: Open User Configuration` and add the same `servers`
565
672
  block under the `mcp` key. Project-level and user-level entries are
566
673
  merged, with project-level taking precedence.
567
674
 
568
675
  ```json
569
676
  {
570
677
  "servers": {
571
- "compress-router": {
678
+ "mcp-compress-router": {
572
679
  "command": "npx",
573
- "args": ["-y", "mcp-compress-router"]
680
+ "args": ["-y", "mcp-compress-router@latest"]
574
681
  }
575
682
  }
576
683
  }
@@ -600,3 +707,11 @@ token catalog, regardless of how many downstream servers you have.
600
707
 
601
708
  For the full configuration and environment variable reference, see
602
709
  [configuration.md](docs/configuration.md).
710
+
711
+ ## Acknowledgements
712
+
713
+ - [mcp2cli](https://github.com/knowsuchagency/mcp2cli) — a very similar
714
+ idea of how MCP can be compressed, but to a CLI.
715
+ - [mcp-compressor](https://github.com/atlassian-labs/mcp-compressor) —
716
+ also a very similar idea; the "two tools" approach was borrowed from
717
+ 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
  });
@@ -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);
@@ -38,7 +38,9 @@ async function startRouterServer(catalog, clients, logger) {
38
38
  const invokeFn = (server, tool, args) => invokeDownstreamTool(clients, server, tool, args, logger);
39
39
  router.registerTool('invoke_tool', {
40
40
  title: 'Invoke Tool',
41
- description: 'Invoke a specific tool on a connected MCP server. First use get_tool_schema to retrieve the required parameters.',
41
+ description: 'Invoke a specific tool on a connected MCP server. ' +
42
+ 'You MUST first use get_tool_schema to retrieve the required parameters ' +
43
+ 'for this tool before calling invoke_tool.',
42
44
  inputSchema: InvokeToolInputSchema,
43
45
  }, createInvokeToolHandler(catalog, invokeFn, logger));
44
46
  const transport = new StdioServerTransport();
@@ -78,12 +80,14 @@ export async function runRouter(configPath, verbose) {
78
80
  })),
79
81
  });
80
82
  const selectionByServer = new Map();
83
+ const compressionLevelByServer = new Map();
81
84
  for (const server of servers) {
82
85
  selectionByServer.set(server.name, {
83
86
  allowedTools: server.allowedTools,
84
87
  disabledTools: server.disabledTools,
85
88
  });
89
+ compressionLevelByServer.set(server.name, server.compressionLevel);
86
90
  }
87
- const catalog = buildCatalog(discovered, selectionByServer, logger);
91
+ const catalog = buildCatalog(discovered, selectionByServer, logger, compressionLevelByServer);
88
92
  await startRouterServer(catalog, clients, logger);
89
93
  }
@@ -14,9 +14,12 @@ import { filterTools } from '../utils/index.js';
14
14
  * server name is absent from the map, all its tools are exposed.
15
15
  * @param logger - Optional logger for unmatched-pattern warnings. When
16
16
  * omitted, no warnings are emitted (backward compatible).
17
+ * @param compressionLevelByServer - Optional per-server compression level
18
+ * map. When a server name is absent or the entry is `undefined`, the
19
+ * level resolves to `high` (the default).
17
20
  * @returns An immutable ToolCatalog containing only exposed tools.
18
21
  */
19
- export function buildCatalog(discovered, selectionByServer = new Map(), logger) {
22
+ export function buildCatalog(discovered, selectionByServer = new Map(), logger, compressionLevelByServer = new Map()) {
20
23
  const toolMap = new Map();
21
24
  const filteredToolNames = new Set();
22
25
  const servers = discovered.map((ds) => {
@@ -37,6 +40,7 @@ export function buildCatalog(discovered, selectionByServer = new Map(), logger)
37
40
  name: ds.name,
38
41
  description: ds.description,
39
42
  tools: exposed,
43
+ compressionLevel: compressionLevelByServer.get(ds.name) ?? 'high',
40
44
  };
41
45
  });
42
46
  return { servers, toolMap, filteredToolNames };
@@ -1,7 +1,7 @@
1
1
  import * as path from 'node:path';
2
2
  import * as os from 'node:os';
3
3
  import * as fs from 'node:fs/promises';
4
- import { expandEnvField, parseJsonc, validateGlobPattern } from '../utils/index.js';
4
+ import { expandEnvField, isCompressionLevel, parseJsonc, validateGlobPattern, VALID_COMPRESSION_LEVELS, } from '../utils/index.js';
5
5
  /** Recognized MCP transport types. */
6
6
  const VALID_TYPES = new Set(['stdio', 'http', 'streamable-http']);
7
7
  /** Per-application directory name used inside the user data folder. */
@@ -243,6 +243,28 @@ function validateEnabled(name, server) {
243
243
  }
244
244
  return server.enabled;
245
245
  }
246
+ /**
247
+ * Validates the optional `compressionLevel` field on a server entry.
248
+ *
249
+ * Accepts `undefined` (absent — resolves to `high` downstream) and the
250
+ * four valid level strings. Any other value (invalid string, number,
251
+ * boolean, null) is a hard error at config load time, matching the
252
+ * existing fail-fast pattern for `type` and `enabled`.
253
+ *
254
+ * @param name - Server name (for error messages).
255
+ * @param value - The raw `compressionLevel` value from the server entry.
256
+ * @returns The validated level, or undefined when the field is absent.
257
+ * @throws If the value is present but not one of max, high, medium, low.
258
+ */
259
+ function validateCompressionLevel(name, value) {
260
+ if (value === undefined)
261
+ return undefined;
262
+ if (!isCompressionLevel(value)) {
263
+ throw new Error(`Server "${name}" has invalid "compressionLevel" value "${String(value)}". ` +
264
+ `Must be one of: ${VALID_COMPRESSION_LEVELS.join(', ')}`);
265
+ }
266
+ return value;
267
+ }
246
268
  /**
247
269
  * Validates an optional tool-name glob list (`allowedTools` or
248
270
  * `disabledTools`).
@@ -310,7 +332,18 @@ function parseServerEntry(name, entry, names) {
310
332
  const enabled = validateEnabled(name, server);
311
333
  const allowedTools = validateToolList(name, 'allowedTools', server.allowedTools);
312
334
  const disabledTools = validateToolList(name, 'disabledTools', server.disabledTools);
313
- return { name, type, ...fields, description, oauth, enabled, allowedTools, disabledTools };
335
+ const compressionLevel = validateCompressionLevel(name, server.compressionLevel);
336
+ return {
337
+ name,
338
+ type,
339
+ ...fields,
340
+ description,
341
+ oauth,
342
+ enabled,
343
+ allowedTools,
344
+ disabledTools,
345
+ compressionLevel,
346
+ };
314
347
  }
315
348
  /**
316
349
  * Loads and validates the MCP configuration file.
@@ -55,5 +55,7 @@ export function createGetToolSchemaHandler(catalog, logger) {
55
55
  */
56
56
  export function buildGetToolSchemaDescription(catalog) {
57
57
  const compact = renderCompactCatalog(catalog.servers);
58
- return 'Get the JSON schema for one or more tools from a connected MCP server.\n\n' + compact;
58
+ return ('Get the JSON schema for one or more tools from a connected MCP server. ' +
59
+ 'You MUST call this for a tool before you can invoke it with invoke_tool.\n\n' +
60
+ compact);
59
61
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Extracts ordered argument names from a tool's `inputSchema`.
3
+ *
4
+ * Reads `inputSchema.properties` keys in definition order. Returns an
5
+ * empty array when the schema is undefined, has no `properties` key,
6
+ * or has a non-object `properties` value (including null). No failure
7
+ * modes — malformed input always yields an empty array.
8
+ *
9
+ * @param inputSchema - The JSON Schema object from a tool descriptor.
10
+ * @returns Ordered argument names (possibly empty).
11
+ */
12
+ export function extractArgumentNames(inputSchema) {
13
+ if (inputSchema === undefined) {
14
+ return [];
15
+ }
16
+ const properties = inputSchema.properties;
17
+ if (properties === undefined || properties === null || typeof properties !== 'object') {
18
+ return [];
19
+ }
20
+ return Object.keys(properties);
21
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Valid `compressionLevel` values, in the order used for error messages
3
+ * and documentation.
4
+ */
5
+ export const VALID_COMPRESSION_LEVELS = [
6
+ 'max',
7
+ 'high',
8
+ 'medium',
9
+ 'low',
10
+ ];
11
+ /**
12
+ * Type guard confirming a value is one of the four valid
13
+ * {@link CompressionLevel} strings (`max`, `high`, `medium`, `low`).
14
+ *
15
+ * Used by both the Config Loader (startup validation) and the `add` CLI
16
+ * command (flag validation) so both check against a single source of
17
+ * truth.
18
+ *
19
+ * @param value - Any value read from config or a CLI flag.
20
+ * @returns True when `value` is a string and one of the four valid levels.
21
+ */
22
+ export function isCompressionLevel(value) {
23
+ return (typeof value === 'string' && VALID_COMPRESSION_LEVELS.includes(value));
24
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Truncates a tool description to the first sentence (text up to the
3
+ * first `.`), capped at 10 words. When the first sentence exceeds 10
4
+ * words, only the first 10 are kept and `...` (three ASCII dots) is
5
+ * appended. When the description is absent or empty, returns an empty
6
+ * string. No failure modes — malformed input always yields an empty
7
+ * string.
8
+ *
9
+ * Used by the catalog text renderer at the `medium` compression level.
10
+ * This is distinct from the character-based `truncateDescription` in
11
+ * `src/cli/tools-command.ts`, which serves the CLI table layout.
12
+ *
13
+ * @param description - The raw tool description, or undefined.
14
+ * @returns The truncated first-sentence snippet (possibly empty).
15
+ */
16
+ export function truncateToFirstSentence(description) {
17
+ if (description === undefined || description.length === 0) {
18
+ return '';
19
+ }
20
+ const firstSentence = description.split('.')[0].trim();
21
+ const words = firstSentence.length === 0 ? [] : firstSentence.split(/\s+/);
22
+ if (words.length === 0) {
23
+ return '';
24
+ }
25
+ if (words.length <= 10) {
26
+ return words.join(' ');
27
+ }
28
+ return `${words.slice(0, 10).join(' ')}...`;
29
+ }
@@ -6,5 +6,6 @@ export { validateGlobPattern } from './validate-glob.js';
6
6
  export { expandEnvField } from './expand-env.js';
7
7
  export { Logger } from './logger.js';
8
8
  export { parseJsonc } from './parse-jsonc.js';
9
+ export { VALID_COMPRESSION_LEVELS, isCompressionLevel } from './compression-level.js';
9
10
  /** @public */
10
11
  export { openBrowser } from './open-browser.js';
@@ -1,33 +1,72 @@
1
+ import { extractArgumentNames } from './argument-names.js';
2
+ import { truncateToFirstSentence } from './description-truncator.js';
1
3
  /**
2
4
  * Renders the compact catalog as Markdown text suitable for inclusion
3
5
  * in the `get_tool_schema` tool description.
4
6
  *
5
- * Format:
6
- * ## {server name}
7
- * {description (optional)}
7
+ * Each server's tools are rendered according to that server's
8
+ * `compressionLevel`:
8
9
  *
9
- * Available tools:
10
- * {tool1}, {tool2}, ...
10
+ * - `max` — tool names only, comma-separated on a single line.
11
+ * - `high` (default) — `toolName(arg1, arg2)`, one tool per line.
12
+ * - `medium` — `toolName(arg1, arg2): first sentence...`, one tool per
13
+ * line (the snippet is omitted when the description is absent).
14
+ * - `low` — `<tool>toolName(arg1, arg2): full description</tool>`,
15
+ * one tool per line (description omitted when absent).
11
16
  *
12
- * When a server has no tools, only the header (and optional
13
- * description) is rendered.
17
+ * Argument names are extracted from each tool's `inputSchema.properties`
18
+ * keys in definition order. When a server has no tools, only the header
19
+ * (and optional description) is rendered.
14
20
  *
15
21
  * @param servers - The catalog server entries.
16
22
  * @returns Compact catalog text.
17
23
  */
18
24
  export function renderCompactCatalog(servers) {
19
- const blocks = [];
20
- for (const server of servers) {
21
- const lines = [`## ${server.name}`];
22
- if (server.description) {
23
- lines.push(server.description);
25
+ return servers.map((server) => renderServerBlock(server)).join('\n\n');
26
+ }
27
+ /**
28
+ * Renders a single server section: header, optional description, and
29
+ * the tool listing formatted for that server's compression level.
30
+ *
31
+ * @param server - The catalog server to render.
32
+ * @returns The server block text.
33
+ */
34
+ function renderServerBlock(server) {
35
+ const lines = [`## ${server.name}`];
36
+ if (server.description) {
37
+ lines.push(server.description);
38
+ }
39
+ if (server.tools.length > 0) {
40
+ lines.push('', 'Available tools:');
41
+ if (server.compressionLevel === 'max') {
42
+ lines.push(server.tools.map((tool) => tool.name).join(', '));
24
43
  }
25
- if (server.tools.length > 0) {
26
- lines.push('');
27
- lines.push('Available tools:');
28
- lines.push(server.tools.map((t) => t.name).join(', '));
44
+ else {
45
+ for (const tool of server.tools) {
46
+ lines.push(renderToolLine(tool, server.compressionLevel));
47
+ }
29
48
  }
30
- blocks.push(lines.join('\n'));
31
49
  }
32
- return blocks.join('\n\n');
50
+ return lines.join('\n');
51
+ }
52
+ /**
53
+ * Renders a single tool line at the `high`, `medium`, or `low` level.
54
+ *
55
+ * @param tool - The tool descriptor.
56
+ * @param level - The compression level (never `max`).
57
+ * @returns The formatted tool line.
58
+ */
59
+ function renderToolLine(tool, level) {
60
+ const args = extractArgumentNames(tool.inputSchema);
61
+ const signature = `${tool.name}(${args.join(', ')})`;
62
+ if (level === 'low') {
63
+ return tool.description
64
+ ? `<tool>${signature}: ${tool.description}</tool>`
65
+ : `<tool>${signature}</tool>`;
66
+ }
67
+ if (level === 'medium') {
68
+ const snippet = truncateToFirstSentence(tool.description);
69
+ return snippet ? `${signature}: ${snippet}` : signature;
70
+ }
71
+ return signature;
33
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-compress-router",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Compress all connected MCP servers into a single router MCP to save tokens",
5
5
  "license": "MIT",
6
6
  "type": "module",