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 +104 -73
- package/build/cli/add-command.js +8 -1
- package/build/cli/get-command.js +6 -0
- package/build/cli/list-command.js +16 -3
- package/build/cli/register-commands.js +2 -0
- package/build/cli/router-runner.js +3 -1
- package/build/services/catalog.js +5 -1
- package/build/services/config.js +35 -2
- package/build/utils/argument-names.js +21 -0
- package/build/utils/compression-level.js +24 -0
- package/build/utils/description-truncator.js +29 -0
- package/build/utils/index.js +1 -0
- package/build/utils/text-format.js +57 -18
- package/package.json +1 -1
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
|
|
101
|
+
npx mcp-compress-router@latest add playwright -- npx -y @playwright/mcp
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
This registers a downstream MCP server named `
|
|
103
|
-
your [config file](#config-file-location). Repeat for every MCP server
|
|
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
|
-
|
|
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
|
-
|
|
611
|
-
|
|
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
|
-
|
|
631
|
-
|
|
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
|
-
`
|
|
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.
|
package/build/cli/add-command.js
CHANGED
|
@@ -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) {
|
package/build/cli/get-command.js
CHANGED
|
@@ -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 };
|
package/build/services/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/build/utils/index.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* {description (optional)}
|
|
7
|
+
* Each server's tools are rendered according to that server's
|
|
8
|
+
* `compressionLevel`:
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
}
|