mcp-compress-router 1.5.2 → 1.5.5
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 +105 -60
- package/build/cli/add-command.js +1 -1
- package/build/cli/register-commands.js +4 -4
- package/build/services/config.js +3 -1
- package/build/services/discovery.js +13 -5
- package/build/services/oauth-discovery.js +7 -2
- package/build/services/server-connection.js +7 -2
- package/build/utils/index.js +1 -0
- package/build/utils/timeout.js +75 -0
- package/build/utils/validate-glob.js +1 -1
- package/package.json +18 -20
package/README.md
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
- [The Solution](#the-solution)
|
|
21
21
|
- [Prerequisites](#prerequisites)
|
|
22
22
|
- [Quick Start](#quick-start)
|
|
23
|
+
- [1. Connect your coding agent](#1-connect-your-coding-agent)
|
|
24
|
+
- [2. Add downstream MCP servers](#2-add-downstream-mcp-servers)
|
|
25
|
+
- [3. Verify and use](#3-verify-and-use)
|
|
23
26
|
- [Configuration](#configuration)
|
|
24
27
|
- [Config File Location](#config-file-location)
|
|
25
28
|
- [Adding Downstream Servers](#adding-downstream-servers)
|
|
@@ -29,17 +32,12 @@
|
|
|
29
32
|
- [Inspecting Tools](#inspecting-tools)
|
|
30
33
|
- [OAuth](#oauth)
|
|
31
34
|
- [Redirect URL](#redirect-url)
|
|
32
|
-
- [GitHub MCP (special case)](#github-mcp-special-case)
|
|
33
|
-
- [Figma MCP (special case)](#figma-mcp-special-case)
|
|
35
|
+
- [GitHub MCP with OAuth (special case)](#github-mcp-with-oauth-special-case)
|
|
36
|
+
- [Figma MCP with OAuth (special case)](#figma-mcp-with-oauth-special-case)
|
|
34
37
|
- [Custom Headers](#custom-headers)
|
|
35
38
|
- [Secrets and Variable Expansion](#secrets-and-variable-expansion)
|
|
36
|
-
- [Connecting Coding Agents](#connecting-coding-agents)
|
|
37
|
-
- [Opencode](#opencode)
|
|
38
|
-
- [Claude Code](#claude-code)
|
|
39
|
-
- [Codex](#codex)
|
|
40
|
-
- [GitHub Copilot](#github-copilot)
|
|
41
39
|
- [How It Works](#how-it-works)
|
|
42
|
-
- [
|
|
40
|
+
- [Acknowledgements](#acknowledgements)
|
|
43
41
|
|
|
44
42
|
## The Problem
|
|
45
43
|
|
|
@@ -95,17 +93,99 @@ have, the more you save.
|
|
|
95
93
|
|
|
96
94
|
The router is published on npm as
|
|
97
95
|
[`mcp-compress-router`](https://www.npmjs.com/package/mcp-compress-router).
|
|
98
|
-
You do not need to install it — just run it with `npx
|
|
96
|
+
You do not need to install it — just run it with `npx`.
|
|
97
|
+
|
|
98
|
+
Setup is two steps: first connect your coding agent to the router, then
|
|
99
|
+
add the MCP servers you want to compress behind it.
|
|
100
|
+
|
|
101
|
+
### 1. Connect your coding agent
|
|
102
|
+
|
|
103
|
+
Point your agent at the router the same way you would point it at any
|
|
104
|
+
other MCP server. The router reads its server list from a
|
|
105
|
+
[user-wide config file](#config-file-location) by default, so register
|
|
106
|
+
it at the **user** scope (or your agent's equivalent) — every project
|
|
107
|
+
then gets the same compressed catalog with no per-project setup.
|
|
108
|
+
|
|
109
|
+
- **Claude Code** — see the
|
|
110
|
+
[Claude Code MCP docs](https://code.claude.com/docs/en/mcp).
|
|
111
|
+
Add it at `user` scope so it applies everywhere (Claude Code also
|
|
112
|
+
supports `local` for a single private project and `project` for a
|
|
113
|
+
shareable `.mcp.json`):
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
claude mcp add mcp-compress-router --scope user -- npx -y mcp-compress-router@latest
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **Opencode** — see the
|
|
120
|
+
[opencode MCP servers docs](https://opencode.ai/docs/mcp-servers/).
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- **Codex** — see the
|
|
127
|
+
[Codex MCP docs](https://developers.openai.com/codex/mcp).
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **GitHub Copilot (VS Code)** — see the
|
|
134
|
+
[VS Code MCP docs](https://code.visualstudio.com/docs/agent-customization/mcp-servers).
|
|
135
|
+
Open the Command Palette (`Cmd+Shift+P`) →
|
|
136
|
+
`MCP: Open User Configuration` and add the server block under
|
|
137
|
+
`mcp.servers` (project-level `.vscode/mcp.json` is also supported
|
|
138
|
+
and merged with the user-level settings, project taking precedence):
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"servers": {
|
|
143
|
+
"mcp-compress-router": {
|
|
144
|
+
"command": "npx",
|
|
145
|
+
"args": ["-y", "mcp-compress-router@latest"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 2. Add downstream MCP servers
|
|
152
|
+
|
|
153
|
+
Use the `add` command to register each MCP server you want to compress.
|
|
154
|
+
The router writes them to its [config file](#config-file-location) and
|
|
155
|
+
they appear in the catalog the next time the router starts.
|
|
99
156
|
|
|
100
157
|
```bash
|
|
101
158
|
npx mcp-compress-router@latest add playwright -- npx -y @playwright/mcp
|
|
102
159
|
```
|
|
103
160
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
161
|
+
Add a description so the LLM can pick the right server, and use
|
|
162
|
+
environment variables to keep secrets out of the config:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx mcp-compress-router@latest add github \
|
|
166
|
+
--description "GitHub API tools" \
|
|
167
|
+
-e GITHUB_PERSONAL_TOKEN=ghp_xxx \
|
|
168
|
+
-- npx -y @modelcontextprotocol/server-github
|
|
169
|
+
```
|
|
107
170
|
|
|
108
|
-
|
|
171
|
+
Repeat for every MCP server you want to compress. See
|
|
172
|
+
[Adding Downstream Servers](#adding-downstream-servers) for HTTP
|
|
173
|
+
servers, custom headers, OAuth, and other options.
|
|
174
|
+
|
|
175
|
+
### 3. Verify and use
|
|
176
|
+
|
|
177
|
+
Check that everything is wired up:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx mcp-compress-router@latest list
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Then start a new session in your coding agent. The agent picks up the
|
|
184
|
+
router's two tools — `get_tool_schema` and `invoke_tool` — and uses
|
|
185
|
+
them to discover and call every server you added, without the agent
|
|
186
|
+
ever seeing the raw tool listings of each downstream server.
|
|
187
|
+
|
|
188
|
+
Under the hood your agent spawns the router as a child process with:
|
|
109
189
|
|
|
110
190
|
```bash
|
|
111
191
|
npx mcp-compress-router@latest
|
|
@@ -113,7 +193,8 @@ npx mcp-compress-router@latest
|
|
|
113
193
|
|
|
114
194
|
When started without a subcommand, the router runs the MCP server over
|
|
115
195
|
stdio and exposes exactly two tools (`get_tool_schema`,
|
|
116
|
-
`invoke_tool`) to the agent.
|
|
196
|
+
`invoke_tool`) to the agent. You normally do not run this yourself —
|
|
197
|
+
your agent spawns the router automatically.
|
|
117
198
|
|
|
118
199
|
## Configuration
|
|
119
200
|
|
|
@@ -338,6 +419,16 @@ A good rule of thumb:
|
|
|
338
419
|
- Use `low` sparingly — only when full descriptions must be visible
|
|
339
420
|
without a `get_tool_schema` call, since it costs the most tokens.
|
|
340
421
|
|
|
422
|
+
> **Note for Claude Code:** Claude Code truncates any single tool
|
|
423
|
+
> description at 2000 characters. The router renders the entire
|
|
424
|
+
> compressed catalog into the `get_tool_schema` description that the
|
|
425
|
+
> agent receives on every turn, so with several downstream servers or
|
|
426
|
+
> many tools per server the `high`, `medium`, or `low` listings can
|
|
427
|
+
> exceed that limit and get truncated — breaking routing. When using
|
|
428
|
+
> Claude Code, set `compressionLevel: "max"` on your servers (or pass
|
|
429
|
+
> `--compression-level max` to `add`) so each server's tool listing
|
|
430
|
+
> collapses to a comma-separated single line.
|
|
431
|
+
|
|
341
432
|
### Inspecting Tools
|
|
342
433
|
|
|
343
434
|
To see exactly which tools a server advertises — and which are
|
|
@@ -640,52 +731,6 @@ MY_SERVER_TOKEN=secret-token
|
|
|
640
731
|
|
|
641
732
|
Shell environment variables always take precedence over `.env` values.
|
|
642
733
|
|
|
643
|
-
## Connecting Coding Agents
|
|
644
|
-
|
|
645
|
-
Once your downstream servers are configured, connect your agent to the
|
|
646
|
-
router the same way you would connect any other MCP server — by
|
|
647
|
-
pointing it at `npx mcp-compress-router@latest`. The examples below assume the
|
|
648
|
-
default [config location](#config-file-location); pass `-c <path>` if
|
|
649
|
-
you use a custom one.
|
|
650
|
-
|
|
651
|
-
### Opencode
|
|
652
|
-
|
|
653
|
-
```sh
|
|
654
|
-
opencode mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### Claude Code
|
|
658
|
-
|
|
659
|
-
```sh
|
|
660
|
-
claude mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
### Codex
|
|
664
|
-
|
|
665
|
-
```sh
|
|
666
|
-
codex mcp add mcp-compress-router -- npx -y mcp-compress-router@latest
|
|
667
|
-
```
|
|
668
|
-
|
|
669
|
-
### GitHub Copilot (VS Code)
|
|
670
|
-
|
|
671
|
-
Add this to `.vscode/mcp.json` in your workspace (project-level, applies
|
|
672
|
-
only to that workspace), or to your **user-level** MCP settings which
|
|
673
|
-
apply across every workspace: open the Command Palette (`Cmd+Shift+P`) →
|
|
674
|
-
`MCP: Open User Configuration` and add the same `servers`
|
|
675
|
-
block under the `mcp` key. Project-level and user-level entries are
|
|
676
|
-
merged, with project-level taking precedence.
|
|
677
|
-
|
|
678
|
-
```json
|
|
679
|
-
{
|
|
680
|
-
"servers": {
|
|
681
|
-
"mcp-compress-router": {
|
|
682
|
-
"command": "npx",
|
|
683
|
-
"args": ["-y", "mcp-compress-router@latest"]
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
```
|
|
688
|
-
|
|
689
734
|
## How It Works
|
|
690
735
|
|
|
691
736
|
Once connected, the agent sees exactly **two tools**:
|
package/build/cli/add-command.js
CHANGED
|
@@ -17,7 +17,7 @@ function validateToolListPatterns(field, patterns) {
|
|
|
17
17
|
}
|
|
18
18
|
catch (err) {
|
|
19
19
|
const reason = err instanceof Error ? err.message : String(err);
|
|
20
|
-
throw new Error(`Invalid "${field}" pattern "${pattern}": ${reason}
|
|
20
|
+
throw new Error(`Invalid "${field}" pattern "${pattern}": ${reason}`, { cause: err });
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -3,7 +3,7 @@ import { handleAdd, handleRemove, handleGet, handleList, handleLogin, handleLogo
|
|
|
3
3
|
import { runRouter } from './router-runner.js';
|
|
4
4
|
/**
|
|
5
5
|
* Collects repeated `--header "K: V"` flags into a headers record.
|
|
6
|
-
* @internal
|
|
6
|
+
* @internal Exported for tests only; not part of the public module API.
|
|
7
7
|
*/
|
|
8
8
|
export function collectHeaders(value, previous) {
|
|
9
9
|
const colonIdx = value.indexOf(':');
|
|
@@ -16,7 +16,7 @@ export function collectHeaders(value, previous) {
|
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* Collects repeated `-e KEY=value` flags into an env record.
|
|
19
|
-
* @internal
|
|
19
|
+
* @internal Exported for tests only; not part of the public module API.
|
|
20
20
|
*/
|
|
21
21
|
export function collectEnv(value, previous) {
|
|
22
22
|
const eqIdx = value.indexOf('=');
|
|
@@ -29,7 +29,7 @@ export function collectEnv(value, previous) {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Collects repeated `--flag <value>` flags into an ordered string array.
|
|
32
|
-
* @internal
|
|
32
|
+
* @internal Exported for tests only; not part of the public module API.
|
|
33
33
|
*/
|
|
34
34
|
export function collectStringArray(value, previous) {
|
|
35
35
|
return [...previous, value];
|
|
@@ -38,7 +38,7 @@ export function collectStringArray(value, previous) {
|
|
|
38
38
|
* Coerces a `--port` flag value into an integer. Throws on non-numeric
|
|
39
39
|
* values so the user gets a clear error before any network activity.
|
|
40
40
|
* Range validation is deferred to the command handlers.
|
|
41
|
-
* @internal
|
|
41
|
+
* @internal Exported for tests only; not part of the public module API.
|
|
42
42
|
*/
|
|
43
43
|
export function parsePort(value) {
|
|
44
44
|
const port = Number(value);
|
package/build/services/config.js
CHANGED
|
@@ -297,7 +297,9 @@ function validateToolList(name, field, value) {
|
|
|
297
297
|
}
|
|
298
298
|
catch (err) {
|
|
299
299
|
const reason = err instanceof Error ? err.message : String(err);
|
|
300
|
-
throw new Error(`Server "${name}" has invalid "${field}" pattern "${entry}": ${reason}
|
|
300
|
+
throw new Error(`Server "${name}" has invalid "${field}" pattern "${entry}": ${reason}`, {
|
|
301
|
+
cause: err,
|
|
302
|
+
});
|
|
301
303
|
}
|
|
302
304
|
patterns.push(entry);
|
|
303
305
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
2
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
3
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { getDownstreamTimeoutMs } from '../utils/index.js';
|
|
4
5
|
/** JSON-RPC error code for "Method not found". */
|
|
5
6
|
const METHOD_NOT_FOUND = -32601;
|
|
6
7
|
/**
|
|
@@ -21,12 +22,14 @@ function isMethodNotFound(err) {
|
|
|
21
22
|
* propagated as a connection failure.
|
|
22
23
|
*
|
|
23
24
|
* @param client - A connected MCP client.
|
|
25
|
+
* @param options - Optional {@link RequestOptions} (e.g. `timeout`)
|
|
26
|
+
* forwarded to `client.listTools()`.
|
|
24
27
|
* @returns The list-tools result (possibly empty).
|
|
25
28
|
* @throws Any non-"Method not found" error from `listTools()`.
|
|
26
29
|
*/
|
|
27
|
-
export async function listToolsOrEmpty(client) {
|
|
30
|
+
export async function listToolsOrEmpty(client, options) {
|
|
28
31
|
try {
|
|
29
|
-
return await client.listTools();
|
|
32
|
+
return await client.listTools(undefined, options);
|
|
30
33
|
}
|
|
31
34
|
catch (err) {
|
|
32
35
|
if (isMethodNotFound(err)) {
|
|
@@ -59,9 +62,14 @@ export async function discoverSingleServer(server, logger, getAuthProvider) {
|
|
|
59
62
|
type: server.type,
|
|
60
63
|
});
|
|
61
64
|
const transport = createTransport(server, getAuthProvider);
|
|
65
|
+
// Cap the initialize handshake and tools/list call so a server that
|
|
66
|
+
// accepts the connection but never replies surfaces a clear error
|
|
67
|
+
// instead of hanging the command indefinitely (the SDK's own default
|
|
68
|
+
// is 60s; we use a shorter, configurable budget).
|
|
69
|
+
const requestOptions = { timeout: getDownstreamTimeoutMs() };
|
|
62
70
|
try {
|
|
63
|
-
await client.connect(transport);
|
|
64
|
-
const listResult = await listToolsOrEmpty(client);
|
|
71
|
+
await client.connect(transport, requestOptions);
|
|
72
|
+
const listResult = await listToolsOrEmpty(client, requestOptions);
|
|
65
73
|
logger.info(`Connected to "${server.name}" — ${listResult.tools.length} tools discovered`, {
|
|
66
74
|
server: server.name,
|
|
67
75
|
toolCount: listResult.tools.length,
|
|
@@ -86,7 +94,7 @@ export async function discoverSingleServer(server, logger, getAuthProvider) {
|
|
|
86
94
|
type: server.type,
|
|
87
95
|
error: message,
|
|
88
96
|
});
|
|
89
|
-
throw new Error(`Failed to connect to server "${server.name}": ${message}
|
|
97
|
+
throw new Error(`Failed to connect to server "${server.name}": ${message}`, { cause: err });
|
|
90
98
|
}
|
|
91
99
|
finally {
|
|
92
100
|
await client.close().catch(() => { });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createTimeoutFetch, getAuthDiscoveryTimeoutMs } from '../utils/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Discovers OAuth metadata for a downstream MCP server following the
|
|
3
4
|
* MCP 2025-06-18 authorization spec two-step flow:
|
|
@@ -29,6 +30,10 @@
|
|
|
29
30
|
*/
|
|
30
31
|
export async function discoverAuth(serverUrl) {
|
|
31
32
|
const { discoverOAuthProtectedResourceMetadata, discoverAuthorizationServerMetadata } = await import('@modelcontextprotocol/sdk/client/auth.js');
|
|
33
|
+
// The SDK discovery helpers use a raw fetch with no timeout; a server
|
|
34
|
+
// that hangs its well-known endpoint would trap discovery forever. Pass
|
|
35
|
+
// a fetch that aborts after a short, configurable budget.
|
|
36
|
+
const fetchFn = createTimeoutFetch(getAuthDiscoveryTimeoutMs());
|
|
32
37
|
// Tracks the last error seen across all candidates so the caller can be
|
|
33
38
|
// notified when discovery failed entirely (vs. cleanly finding nothing).
|
|
34
39
|
let lastError;
|
|
@@ -36,7 +41,7 @@ export async function discoverAuth(serverUrl) {
|
|
|
36
41
|
// recorded and treated as "not found" so the next candidate is tried.
|
|
37
42
|
const safeDiscoverAs = async (url) => {
|
|
38
43
|
try {
|
|
39
|
-
return await discoverAuthorizationServerMetadata(url);
|
|
44
|
+
return await discoverAuthorizationServerMetadata(url, { fetchFn });
|
|
40
45
|
}
|
|
41
46
|
catch (err) {
|
|
42
47
|
lastError = err;
|
|
@@ -49,7 +54,7 @@ export async function discoverAuth(serverUrl) {
|
|
|
49
54
|
// legacy-server path, not a probe failure.
|
|
50
55
|
let resourceMetadata;
|
|
51
56
|
try {
|
|
52
|
-
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl);
|
|
57
|
+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, {}, fetchFn);
|
|
53
58
|
}
|
|
54
59
|
catch {
|
|
55
60
|
// No PRM published; fall through to direct AS discovery below.
|
|
@@ -3,6 +3,7 @@ import { createTransport, listToolsOrEmpty } from './discovery.js';
|
|
|
3
3
|
import { OAuthCredentialManager } from './oauth.js';
|
|
4
4
|
import { saveToolCache, loadToolCache } from './tool-cache.js';
|
|
5
5
|
import { isAuthError } from './index.js';
|
|
6
|
+
import { getDownstreamTimeoutMs } from '../utils/index.js';
|
|
6
7
|
/**
|
|
7
8
|
* 30-second cooldown after a failed reconnect attempt. Subsequent
|
|
8
9
|
* `invokeWithRecovery` calls within this window return the cached
|
|
@@ -218,8 +219,12 @@ export class ServerConnection {
|
|
|
218
219
|
? (_s) => this.authProvider
|
|
219
220
|
: undefined;
|
|
220
221
|
const transport = createTransport(this.server, getAuthProvider);
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
// Cap the initialize handshake and tools/list call so a hung
|
|
223
|
+
// downstream server fails fast instead of stalling startup (or
|
|
224
|
+
// self-recovery) for the SDK's 60s default.
|
|
225
|
+
const requestOptions = { timeout: getDownstreamTimeoutMs() };
|
|
226
|
+
await this.client.connect(transport, requestOptions);
|
|
227
|
+
const listResult = await listToolsOrEmpty(this.client, requestOptions);
|
|
223
228
|
return listResult.tools.map((t) => ({
|
|
224
229
|
name: t.name,
|
|
225
230
|
description: t.description,
|
package/build/utils/index.js
CHANGED
|
@@ -6,3 +6,4 @@ export { expandEnvField } from './expand-env.js';
|
|
|
6
6
|
export { Logger } from './logger.js';
|
|
7
7
|
export { parseJsonc } from './parse-jsonc.js';
|
|
8
8
|
export { VALID_COMPRESSION_LEVELS, isCompressionLevel } from './compression-level.js';
|
|
9
|
+
export { getDownstreamTimeoutMs, getAuthDiscoveryTimeoutMs, createTimeoutFetch, } from './timeout.js';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
/**
|
|
3
|
+
* Default timeout (ms) for a downstream server's `initialize` request and
|
|
4
|
+
* `tools/list` call during discovery. The MCP SDK's own default is 60s;
|
|
5
|
+
* we cap these fast handshake operations sooner so a server that accepts
|
|
6
|
+
* the TCP/TLS connection but never replies (a real-world hang observed on
|
|
7
|
+
* some Streamable HTTP servers behind CDNs) surfaces a clear error instead
|
|
8
|
+
* of blocking the `tools` command or router startup indefinitely.
|
|
9
|
+
*
|
|
10
|
+
* @internal Exported for tests only; not part of the public module API.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_DOWNSTREAM_TIMEOUT_MS = 30_000;
|
|
13
|
+
/**
|
|
14
|
+
* Default timeout (ms) for OAuth metadata discovery probes (RFC 9728 /
|
|
15
|
+
* RFC 8414 well-known endpoint fetches). These are best-effort probes;
|
|
16
|
+
* a server that hangs its well-known endpoint must not stall startup.
|
|
17
|
+
*
|
|
18
|
+
* @internal Exported for tests only; not part of the public module API.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_AUTH_DISCOVERY_TIMEOUT_MS = 10_000;
|
|
21
|
+
/**
|
|
22
|
+
* Reads a positive-integer environment variable, returning `undefined`
|
|
23
|
+
* when unset or invalid (non-integer or non-positive). Invalid values
|
|
24
|
+
* silently fall back to the default rather than aborting startup.
|
|
25
|
+
*/
|
|
26
|
+
function readPositiveIntEnv(name) {
|
|
27
|
+
const raw = process.env[name];
|
|
28
|
+
if (raw === undefined) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const value = Number(raw);
|
|
32
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the downstream connect/listTools timeout in milliseconds.
|
|
39
|
+
* Override with `MCP_COMPRESS_ROUTER_DOWNSTREAM_TIMEOUT_MS` (a positive
|
|
40
|
+
* integer); invalid values fall back to
|
|
41
|
+
* {@link DEFAULT_DOWNSTREAM_TIMEOUT_MS}.
|
|
42
|
+
*/
|
|
43
|
+
export function getDownstreamTimeoutMs() {
|
|
44
|
+
return (readPositiveIntEnv('MCP_COMPRESS_ROUTER_DOWNSTREAM_TIMEOUT_MS') ?? DEFAULT_DOWNSTREAM_TIMEOUT_MS);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the OAuth discovery probe timeout in milliseconds. Override
|
|
48
|
+
* with `MCP_COMPRESS_ROUTER_AUTH_DISCOVERY_TIMEOUT_MS` (a positive
|
|
49
|
+
* integer); invalid values fall back to
|
|
50
|
+
* {@link DEFAULT_AUTH_DISCOVERY_TIMEOUT_MS}.
|
|
51
|
+
*/
|
|
52
|
+
export function getAuthDiscoveryTimeoutMs() {
|
|
53
|
+
return (readPositiveIntEnv('MCP_COMPRESS_ROUTER_AUTH_DISCOVERY_TIMEOUT_MS') ??
|
|
54
|
+
DEFAULT_AUTH_DISCOVERY_TIMEOUT_MS);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Wraps the global `fetch` so each request aborts after `timeoutMs`.
|
|
58
|
+
* The caller's own `init.signal` (if any) is honored alongside the
|
|
59
|
+
* timeout signal via `AbortSignal.any`.
|
|
60
|
+
*
|
|
61
|
+
* Used to add a request-level timeout to SDK helpers (OAuth metadata
|
|
62
|
+
* discovery) that accept a custom `fetchFn` but expose no timeout option
|
|
63
|
+
* of their own — without it, a server that hangs the well-known endpoint
|
|
64
|
+
* traps discovery forever.
|
|
65
|
+
*
|
|
66
|
+
* @param timeoutMs - Per-request timeout in milliseconds.
|
|
67
|
+
* @returns A fetch-compatible function that aborts on timeout.
|
|
68
|
+
*/
|
|
69
|
+
export function createTimeoutFetch(timeoutMs) {
|
|
70
|
+
return (input, init) => {
|
|
71
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
72
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
73
|
+
return fetch(input, { ...init, signal });
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -16,6 +16,6 @@ export function validateGlobPattern(pattern) {
|
|
|
16
16
|
}
|
|
17
17
|
catch (err) {
|
|
18
18
|
const reason = err instanceof Error ? err.message : String(err);
|
|
19
|
-
throw new Error(`Invalid glob pattern "${pattern}": ${reason}
|
|
19
|
+
throw new Error(`Invalid glob pattern "${pattern}": ${reason}`, { cause: err });
|
|
20
20
|
}
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-compress-router",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "Compress all connected MCP servers into a single router MCP to save tokens",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"url": "https://github.com/ameshkov/mcp-compress-router/issues"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
29
|
-
"typecheck": "tsc && tsc
|
|
28
|
+
"build": "tsc -p tsconfig.app.json && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.app.json --noEmit && tsc -p tsconfig.test.json",
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"test:watch": "vitest",
|
|
32
32
|
"clean": "rm -rf node_modules build",
|
|
33
33
|
"format:check": "prettier --check --no-error-on-unmatched-pattern --cache . && markdownlint-cli2 .",
|
|
34
34
|
"format:fix": "prettier --write --no-error-on-unmatched-pattern --cache . && markdownlint-cli2 --fix .",
|
|
35
|
-
"lint": "
|
|
36
|
-
"lint:fix": "
|
|
35
|
+
"lint": "oxlint src && pnpm knip",
|
|
36
|
+
"lint:fix": "oxlint src --fix",
|
|
37
37
|
"knip": "knip --production",
|
|
38
|
-
"check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run test",
|
|
38
|
+
"check": "pnpm run format:check && pnpm run lint && pnpm run typecheck && pnpm run build && pnpm run test",
|
|
39
39
|
"prepare": "husky || true"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
@@ -51,25 +51,23 @@
|
|
|
51
51
|
],
|
|
52
52
|
"author": "Andrey Meshkov <am@adguard.com>",
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@
|
|
55
|
-
"@types/node": "24.13.2",
|
|
54
|
+
"@types/node": "26.1.2",
|
|
56
55
|
"@types/picomatch": "4.0.3",
|
|
57
|
-
"eslint": "9.16.0",
|
|
58
56
|
"husky": "9.1.7",
|
|
59
|
-
"knip": "6.
|
|
60
|
-
"markdownlint-cli2": "0.
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"typescript
|
|
65
|
-
"vitest": "
|
|
57
|
+
"knip": "6.29.0",
|
|
58
|
+
"markdownlint-cli2": "0.23.2",
|
|
59
|
+
"oxlint": "1.76.0",
|
|
60
|
+
"prettier": "3.9.6",
|
|
61
|
+
"tsx": "4.23.1",
|
|
62
|
+
"typescript": "7.0.2",
|
|
63
|
+
"vitest": "4.1.10"
|
|
66
64
|
},
|
|
67
65
|
"dependencies": {
|
|
68
|
-
"@modelcontextprotocol/sdk": "1.
|
|
69
|
-
"commander": "
|
|
66
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
67
|
+
"commander": "15.0.0",
|
|
70
68
|
"dotenv": "17.4.2",
|
|
71
69
|
"jsonc-parser": "3.3.1",
|
|
72
|
-
"picomatch": "4.0.
|
|
73
|
-
"zod": "
|
|
70
|
+
"picomatch": "4.0.5",
|
|
71
|
+
"zod": "4.4.3"
|
|
74
72
|
}
|
|
75
73
|
}
|