mcp-compress-router 1.3.1 → 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,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
@@ -164,11 +166,11 @@ far better than a bare name.
164
166
  **stdio server** (a local process):
165
167
 
166
168
  ```bash
167
- npx mcp-compress-router add github --description "GitHub API tools" \
169
+ npx mcp-compress-router@latest add github --description "GitHub API tools" \
168
170
  -- npx -y @modelcontextprotocol/server-github
169
171
 
170
172
  # With environment variables
171
- 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 \
172
174
  --description "GitHub API tools" \
173
175
  -- npx -y @modelcontextprotocol/server-github
174
176
  ```
@@ -177,10 +179,10 @@ npx mcp-compress-router add github -e GITHUB_PERSONAL_TOKEN=ghp_xxx \
177
179
  URL):
178
180
 
179
181
  ```bash
180
- npx mcp-compress-router add my-http https://localhost:3100/mcp
182
+ npx mcp-compress-router@latest add my-http https://localhost:3100/mcp
181
183
 
182
184
  # With a custom header
183
- npx mcp-compress-router add my-http \
185
+ npx mcp-compress-router@latest add my-http \
184
186
  --header "Authorization: Bearer mytoken" \
185
187
  https://localhost:3100/mcp
186
188
  ```
@@ -213,9 +215,9 @@ supported. CLI commands write plain `.json`; hand-edited files may use
213
215
  Other management commands:
214
216
 
215
217
  ```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
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
219
221
  ```
220
222
 
221
223
  ### Per-Server Enable/Disable
@@ -230,14 +232,14 @@ enabled, keeping `mcp.json` clean and fully backward compatible.
230
232
  Toggle it from the CLI without touching the rest of the config:
231
233
 
232
234
  ```bash
233
- npx mcp-compress-router disable github # writes "enabled": false
234
- 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
235
237
  ```
236
238
 
237
239
  You can also set it at creation time:
238
240
 
239
241
  ```bash
240
- npx mcp-compress-router add archive --disabled -- npx -y server-archive
242
+ npx mcp-compress-router@latest add archive --disabled -- npx -y server-archive
241
243
  ```
242
244
 
243
245
  ### Per-Server Tool Selection
@@ -274,12 +276,67 @@ hard error at startup. Set filters at creation time with repeatable
274
276
  flags:
275
277
 
276
278
  ```bash
277
- npx mcp-compress-router add github \
279
+ npx mcp-compress-router@latest add github \
278
280
  --allowed-tools list_issues \
279
281
  --allowed-tools get_pull_request \
280
282
  -- npx -y server-github
281
283
  ```
282
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
+
283
340
  ### Inspecting Tools
284
341
 
285
342
  To see exactly which tools a server advertises — and which are
@@ -287,7 +344,7 @@ To see exactly which tools a server advertises — and which are
287
344
  it live without starting the full router:
288
345
 
289
346
  ```bash
290
- npx mcp-compress-router tools github
347
+ npx mcp-compress-router@latest tools github
291
348
  ```
292
349
 
293
350
  This works regardless of the server's `enabled` state (inspecting a
@@ -304,7 +361,7 @@ flow automatically if OAuth is advertised. You can also trigger it
304
361
  manually:
305
362
 
306
363
  ```bash
307
- npx mcp-compress-router login my-http
364
+ npx mcp-compress-router@latest login my-http
308
365
  ```
309
366
 
310
367
  This opens your browser to complete the authorization-code flow. Tokens
@@ -354,7 +411,7 @@ ignore the port on `localhost`. If your provider demands a redirect URI
354
411
  with an **exact port**, pin it with `--port`:
355
412
 
356
413
  ```bash
357
- npx mcp-compress-router login my-http --port 8765
414
+ npx mcp-compress-router@latest login my-http --port 8765
358
415
  ```
359
416
 
360
417
  This binds the callback server to `8765`, so the redirect URI becomes
@@ -374,7 +431,7 @@ flag each time:
374
431
  `--port` overrides `oauth.callbackPort` for a single run. Pass `--port 0`
375
432
  to force an OS-assigned port even when `oauth.callbackPort` is set.
376
433
 
377
- #### GitHub MCP (special case)
434
+ #### GitHub MCP with OAuth (special case)
378
435
 
379
436
  The official GitHub MCP server at
380
437
  `https://api.githubcopilot.com/mcp` advertises OAuth but does **not**
@@ -392,7 +449,7 @@ organizations you want the MCP to access.
392
449
  3. **Add the GitHub MCP server by URL.**
393
450
 
394
451
  ```bash
395
- npx mcp-compress-router add github https://api.githubcopilot.com/mcp
452
+ npx mcp-compress-router@latest add github https://api.githubcopilot.com/mcp
396
453
  ```
397
454
 
398
455
  4. **Set `oauth` credentials in `mcp.json`.**
@@ -419,7 +476,7 @@ organizations you want the MCP to access.
419
476
  5. **Run the login command.**
420
477
 
421
478
  ```bash
422
- npx mcp-compress-router login github
479
+ npx mcp-compress-router@latest login github
423
480
  ```
424
481
 
425
482
  Your browser opens to authorize. After you approve, tokens are stored
@@ -430,7 +487,7 @@ organizations you want the MCP to access.
430
487
  > login will succeed, and its client secret is generated under *General*
431
488
  > → *Generate a new client secret*.
432
489
 
433
- #### Figma MCP (special case)
490
+ #### Figma MCP with OAuth (special case)
434
491
 
435
492
  The official Figma MCP server at `https://mcp.figma.com/mcp` does **not**
436
493
  support Dynamic Client Registration through the standard MCP flow.
@@ -480,7 +537,7 @@ pin it with `oauth.callbackPort`.
480
537
  3. **Add the Figma MCP server by URL.**
481
538
 
482
539
  ```bash
483
- npx mcp-compress-router add --transport http figma https://mcp.figma.com/mcp
540
+ npx mcp-compress-router@latest add --transport http figma https://mcp.figma.com/mcp
484
541
  ```
485
542
 
486
543
  4. **Set `oauth` credentials in `mcp.json`.**
@@ -507,7 +564,7 @@ pin it with `oauth.callbackPort`.
507
564
  5. **Run the login command.**
508
565
 
509
566
  ```bash
510
- npx mcp-compress-router login figma
567
+ npx mcp-compress-router@latest login figma
511
568
  ```
512
569
 
513
570
  Your browser opens to authorize. After you approve, tokens are stored
@@ -516,7 +573,7 @@ pin it with `oauth.callbackPort`.
516
573
  Other OAuth commands:
517
574
 
518
575
  ```bash
519
- npx mcp-compress-router logout my-http # remove stored credentials
576
+ npx mcp-compress-router@latest logout my-http # remove stored credentials
520
577
  ```
521
578
 
522
579
  For headless or CI environments, override the browser with the
@@ -525,7 +582,7 @@ URL is appended as a single final argument (no shell):
525
582
 
526
583
  ```bash
527
584
  MCP_COMPRESS_ROUTER_BROWSER="node /path/to/headless-browser.js" \
528
- npx mcp-compress-router login my-http
585
+ npx mcp-compress-router@latest login my-http
529
586
  ```
530
587
 
531
588
  The default login timeout is 120 seconds; override it with
@@ -538,7 +595,7 @@ token instead of OAuth, use the `headers` field. You can set it via the
538
595
  CLI or directly in `mcp.json`:
539
596
 
540
597
  ```bash
541
- npx mcp-compress-router add my-http \
598
+ npx mcp-compress-router@latest add my-http \
542
599
  --header "Authorization: Bearer mytoken" \
543
600
  --header "X-Custom: value" \
544
601
  https://example.com/mcp
@@ -584,68 +641,34 @@ Shell environment variables always take precedence over `.env` values.
584
641
 
585
642
  Once your downstream servers are configured, connect your agent to the
586
643
  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
644
+ pointing it at `npx mcp-compress-router@latest`. The examples below assume the
588
645
  default [config location](#config-file-location); pass `-c <path>` if
589
646
  you use a custom one.
590
647
 
591
648
  ### Opencode
592
649
 
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
- }
650
+ ```sh
651
+ opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
606
652
  ```
607
653
 
608
654
  ### Claude Code
609
655
 
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
- }
656
+ ```sh
657
+ claude mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
626
658
  ```
627
659
 
628
660
  ### Codex
629
661
 
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
662
+ ```sh
663
+ codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
641
664
  ```
642
665
 
643
- ### GitHub Copilot
666
+ ### GitHub Copilot (VS Code)
644
667
 
645
668
  Add this to `.vscode/mcp.json` in your workspace (project-level, applies
646
669
  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`
670
+ apply across every workspace: open the Command Palette (`Cmd+Shift+P`) →
671
+ `MCP: Open User Configuration` and add the same `servers`
649
672
  block under the `mcp` key. Project-level and user-level entries are
650
673
  merged, with project-level taking precedence.
651
674
 
@@ -654,7 +677,7 @@ merged, with project-level taking precedence.
654
677
  "servers": {
655
678
  "mcp-compress-router": {
656
679
  "command": "npx",
657
- "args": ["-y", "mcp-compress-router"]
680
+ "args": ["-y", "mcp-compress-router@latest"]
658
681
  }
659
682
  }
660
683
  }
@@ -684,3 +707,11 @@ token catalog, regardless of how many downstream servers you have.
684
707
 
685
708
  For the full configuration and environment variable reference, see
686
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);
@@ -80,12 +80,14 @@ export async function runRouter(configPath, verbose) {
80
80
  })),
81
81
  });
82
82
  const selectionByServer = new Map();
83
+ const compressionLevelByServer = new Map();
83
84
  for (const server of servers) {
84
85
  selectionByServer.set(server.name, {
85
86
  allowedTools: server.allowedTools,
86
87
  disabledTools: server.disabledTools,
87
88
  });
89
+ compressionLevelByServer.set(server.name, server.compressionLevel);
88
90
  }
89
- const catalog = buildCatalog(discovered, selectionByServer, logger);
91
+ const catalog = buildCatalog(discovered, selectionByServer, logger, compressionLevelByServer);
90
92
  await startRouterServer(catalog, clients, logger);
91
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.
@@ -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.1",
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",