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