@aiwerk/mcp-bridge 2.1.4 → 2.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 +39 -2
- package/dist/src/index.d.ts +1 -1
- package/dist/src/mcp-router.d.ts +6 -3
- package/dist/src/mcp-router.js +37 -10
- package/dist/src/security.js +17 -16
- package/dist/src/standalone-server.d.ts +2 -0
- package/dist/src/standalone-server.js +20 -15
- package/dist/src/transport-base.d.ts +5 -2
- package/dist/src/transport-base.js +9 -2
- package/dist/src/transport-sse.d.ts +2 -2
- package/dist/src/transport-sse.js +3 -4
- package/dist/src/transport-stdio.js +10 -4
- package/dist/src/transport-streamable-http.d.ts +2 -2
- package/dist/src/transport-streamable-http.js +3 -4
- package/dist/src/types.d.ts +6 -1
- package/dist/src/types.js +5 -6
- package/package.json +2 -2
- package/scripts/validate-recipes.sh +13 -0
- package/servers/apify/recipe.json +20 -7
- package/servers/atlassian/recipe.json +20 -8
- package/servers/chrome-devtools/recipe.json +22 -8
- package/servers/github/recipe.json +20 -8
- package/servers/google-maps/recipe.json +25 -10
- package/servers/hetzner/recipe.json +23 -9
- package/servers/hostinger/recipe.json +24 -9
- package/servers/imap-email/README.md +37 -0
- package/servers/imap-email/recipe.json +47 -6
- package/servers/index.json +292 -71
- package/servers/linear/recipe.json +23 -9
- package/servers/miro/recipe.json +26 -9
- package/servers/notion/recipe.json +24 -9
- package/servers/stripe/recipe.json +26 -9
- package/servers/tavily/recipe.json +24 -9
- package/servers/todoist/recipe.json +24 -9
- package/servers/wise/recipe.json +23 -9
- package/servers/atlassian/config.json +0 -19
- package/servers/chrome-devtools/config.json +0 -16
- package/servers/github/config.json +0 -21
- package/servers/hostinger/config.json +0 -17
- package/servers/wise/config.json +0 -16
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Most AI agents connect to MCP servers one-by-one. With 10+ servers, that's 10+ c
|
|
|
17
17
|
- **Intent routing**: say what you need in plain language, the bridge finds the right tool
|
|
18
18
|
- **Schema compression**: tool descriptions compressed ~57%, full schema on demand
|
|
19
19
|
- **Security layer**: trust levels, tool deny/allow lists, result size limits
|
|
20
|
-
- **HTTP auth**: bearer token, custom headers,
|
|
20
|
+
- **HTTP auth**: bearer token, custom headers, **OAuth2 Client Credentials**, and **OAuth2 Authorization Code + PKCE** (interactive browser login)
|
|
21
21
|
- **Result caching**: LRU cache with per-tool TTL overrides
|
|
22
22
|
- **Batch calls**: parallel multi-tool execution via `action=batch`
|
|
23
23
|
- **Multi-server resolution**: automatic tool disambiguation when multiple servers provide the same tool
|
|
@@ -67,7 +67,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
67
67
|
|
|
68
68
|
## Recipe Spec v2
|
|
69
69
|
|
|
70
|
-
Bundled servers now ship with `recipe.json` using **Universal Recipe Spec v2.
|
|
70
|
+
Bundled servers now ship with `recipe.json` using **Universal Recipe Spec v2.1** (15 servers with rich metadata: category, subcategory, origin, countries, audience, sideEffects).
|
|
71
71
|
During install, MCP Bridge prefers `recipe.json` when present and falls back to legacy `config.json` (v1) for backwards compatibility.
|
|
72
72
|
|
|
73
73
|
- Spec: [`docs/universal-recipe-spec.md`](docs/universal-recipe-spec.md)
|
|
@@ -398,6 +398,37 @@ SSE and streamable-HTTP transports support three auth methods:
|
|
|
398
398
|
|
|
399
399
|
OAuth2 features: automatic token acquisition, caching with expiry-aware refresh, single-attempt 401 retry, env var substitution in credentials.
|
|
400
400
|
|
|
401
|
+
**OAuth2 Authorization Code + PKCE** (interactive browser login):
|
|
402
|
+
|
|
403
|
+
For MCP servers behind enterprise SSO or user-level OAuth2 that require browser-based login:
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"auth": {
|
|
408
|
+
"type": "oauth2",
|
|
409
|
+
"grantType": "authorization_code",
|
|
410
|
+
"authorizationUrl": "https://auth.example.com/authorize",
|
|
411
|
+
"tokenUrl": "https://auth.example.com/oauth/token",
|
|
412
|
+
"clientId": "optional-public-client-id",
|
|
413
|
+
"scopes": ["read", "write"]
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Then authenticate via CLI:
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
mcp-bridge auth login my-server # Opens browser, completes OAuth2 flow
|
|
422
|
+
mcp-bridge auth status # Check token status for all servers
|
|
423
|
+
mcp-bridge auth logout my-server # Remove stored token
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Features:
|
|
427
|
+
- **PKCE (RFC 7636)** — mandatory S256 code challenge, no `clientSecret` needed for public clients
|
|
428
|
+
- **Persistent tokens** — stored in `~/.mcp-bridge/tokens/` (chmod 600), survive bridge restarts
|
|
429
|
+
- **Automatic refresh** — tokens refreshed transparently via `refresh_token` grant
|
|
430
|
+
- **Actionable errors** — expired tokens return error with exact CLI command to re-authenticate
|
|
431
|
+
|
|
401
432
|
### Environment variables
|
|
402
433
|
|
|
403
434
|
Secrets go in `~/.mcp-bridge/.env` (chmod 600 on init):
|
|
@@ -427,6 +458,10 @@ mcp-bridge servers # List configured servers
|
|
|
427
458
|
mcp-bridge search <query> # Search catalog by keyword
|
|
428
459
|
mcp-bridge update [--check] # Check for / install updates
|
|
429
460
|
mcp-bridge --version # Print version
|
|
461
|
+
|
|
462
|
+
mcp-bridge auth login <server> # OAuth2 browser login (Authorization Code + PKCE)
|
|
463
|
+
mcp-bridge auth logout <server> # Remove stored token
|
|
464
|
+
mcp-bridge auth status # Show auth status for all servers
|
|
430
465
|
```
|
|
431
466
|
|
|
432
467
|
## Server Catalog
|
|
@@ -517,6 +552,8 @@ For production deployments with high security requirements, consider adding an e
|
|
|
517
552
|
| ✅ | HTTP auth (bearer, headers) | 2.0.0 |
|
|
518
553
|
| ✅ | Configurable retries + graceful shutdown | 2.0.0 |
|
|
519
554
|
| ✅ | OAuth2 Client Credentials | 2.1.0 |
|
|
555
|
+
| ✅ | OAuth2 Authorization Code + PKCE | 2.5.0 |
|
|
556
|
+
| 🔜 | OAuth2 Device Code flow (headless) | planned |
|
|
520
557
|
| 🔜 | Hosted bridge (bridge.aiwerk.ch) | planned |
|
|
521
558
|
| 🔜 | Remote catalog integration | planned |
|
|
522
559
|
| 🔜 | OpenTelemetry / Prometheus metrics | planned |
|
package/dist/src/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type { ToolResolutionResult, ToolResolutionCandidate } from "./tool-resol
|
|
|
13
13
|
export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
|
|
14
14
|
export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
|
|
15
15
|
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
|
|
16
|
-
export type { Logger, McpServerConfig, McpClientConfig, HttpAuthConfig, RetryConfig, McpTool, McpRequest, McpCallRequest, McpResponse, JsonRpcMessage, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
|
|
16
|
+
export type { Logger, McpServerConfig, McpClientConfig, HttpAuthConfig, RetryConfig, McpTool, McpRequest, McpCallRequest, McpResponse, JsonRpcMessage, McpTransport, McpServerConnection, BridgeConfig, RequestIdState, RequestIdGenerator, } from "./types.js";
|
|
17
17
|
export { nextRequestId } from "./types.js";
|
|
18
18
|
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
19
19
|
export { StandaloneServer } from "./standalone-server.js";
|
package/dist/src/mcp-router.d.ts
CHANGED
|
@@ -93,9 +93,9 @@ export type RouterDispatchResponse = {
|
|
|
93
93
|
code?: number;
|
|
94
94
|
};
|
|
95
95
|
export interface RouterTransportRefs {
|
|
96
|
-
sse: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager) => McpTransport;
|
|
97
|
-
stdio: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void
|
|
98
|
-
streamableHttp: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager) => McpTransport;
|
|
96
|
+
sse: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: () => number) => McpTransport;
|
|
97
|
+
stdio: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, requestIdGenerator?: () => number) => McpTransport;
|
|
98
|
+
streamableHttp: new (config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: () => number) => McpTransport;
|
|
99
99
|
}
|
|
100
100
|
export declare class McpRouter {
|
|
101
101
|
private readonly servers;
|
|
@@ -103,12 +103,14 @@ export declare class McpRouter {
|
|
|
103
103
|
private readonly logger;
|
|
104
104
|
private readonly transportRefs;
|
|
105
105
|
private readonly idleTimeoutMs;
|
|
106
|
+
private readonly connectErrorCooldownMs;
|
|
106
107
|
private readonly maxConcurrent;
|
|
107
108
|
private readonly resultCache;
|
|
108
109
|
private readonly maxBatchSize;
|
|
109
110
|
private readonly states;
|
|
110
111
|
private readonly toolResolver;
|
|
111
112
|
private readonly tokenManager;
|
|
113
|
+
private readonly requestIdState;
|
|
112
114
|
private intentRouter;
|
|
113
115
|
private promotion;
|
|
114
116
|
constructor(servers: Record<string, McpServerConfig>, clientConfig: McpClientConfig, logger: Logger, transportRefs?: Partial<RouterTransportRefs>);
|
|
@@ -134,6 +136,7 @@ export declare class McpRouter {
|
|
|
134
136
|
private enforceMaxConcurrent;
|
|
135
137
|
private disconnectServer;
|
|
136
138
|
private markUsed;
|
|
139
|
+
private nextRequestId;
|
|
137
140
|
private createTransport;
|
|
138
141
|
private extractRequiredParams;
|
|
139
142
|
private error;
|
package/dist/src/mcp-router.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nextRequestId, } from "./types.js";
|
|
1
2
|
import { SseTransport } from "./transport-sse.js";
|
|
2
3
|
import { StdioTransport } from "./transport-stdio.js";
|
|
3
4
|
import { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
@@ -11,6 +12,7 @@ import { ResultCache, createResultCacheKey } from "./result-cache.js";
|
|
|
11
12
|
import { ToolResolver } from "./tool-resolution.js";
|
|
12
13
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
13
14
|
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
15
|
+
const DEFAULT_CONNECT_ERROR_COOLDOWN_MS = 10 * 1000;
|
|
14
16
|
const DEFAULT_MAX_CONCURRENT = 5;
|
|
15
17
|
const DEFAULT_MAX_BATCH_SIZE = 10;
|
|
16
18
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
@@ -20,12 +22,14 @@ export class McpRouter {
|
|
|
20
22
|
logger;
|
|
21
23
|
transportRefs;
|
|
22
24
|
idleTimeoutMs;
|
|
25
|
+
connectErrorCooldownMs;
|
|
23
26
|
maxConcurrent;
|
|
24
27
|
resultCache;
|
|
25
28
|
maxBatchSize;
|
|
26
29
|
states = new Map();
|
|
27
30
|
toolResolver;
|
|
28
31
|
tokenManager;
|
|
32
|
+
requestIdState = { value: 0 };
|
|
29
33
|
intentRouter = null;
|
|
30
34
|
promotion = null;
|
|
31
35
|
constructor(servers, clientConfig, logger, transportRefs) {
|
|
@@ -38,6 +42,7 @@ export class McpRouter {
|
|
|
38
42
|
streamableHttp: transportRefs?.streamableHttp ?? StreamableHttpTransport
|
|
39
43
|
};
|
|
40
44
|
this.idleTimeoutMs = clientConfig.routerIdleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
45
|
+
this.connectErrorCooldownMs = clientConfig.routerConnectErrorCooldownMs ?? DEFAULT_CONNECT_ERROR_COOLDOWN_MS;
|
|
41
46
|
this.maxConcurrent = clientConfig.routerMaxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
42
47
|
this.resultCache = clientConfig.resultCache?.enabled
|
|
43
48
|
? new ResultCache({
|
|
@@ -505,20 +510,39 @@ export class McpRouter {
|
|
|
505
510
|
};
|
|
506
511
|
this.states.set(server, state);
|
|
507
512
|
}
|
|
513
|
+
const lastConnectError = state.lastConnectError;
|
|
514
|
+
if (lastConnectError) {
|
|
515
|
+
const withinCooldown = Date.now() - lastConnectError.timestamp < this.connectErrorCooldownMs;
|
|
516
|
+
if (withinCooldown) {
|
|
517
|
+
throw lastConnectError.error;
|
|
518
|
+
}
|
|
519
|
+
state.lastConnectError = undefined;
|
|
520
|
+
}
|
|
508
521
|
if (state.initPromise) {
|
|
509
522
|
await state.initPromise;
|
|
510
523
|
return state;
|
|
511
524
|
}
|
|
512
525
|
state.initPromise = (async () => {
|
|
513
|
-
|
|
514
|
-
|
|
526
|
+
try {
|
|
527
|
+
if (!state.transport.isConnected()) {
|
|
528
|
+
await state.transport.connect();
|
|
529
|
+
}
|
|
530
|
+
if (!state.initialized) {
|
|
531
|
+
await initializeProtocol(state.transport, PACKAGE_VERSION);
|
|
532
|
+
state.initialized = true;
|
|
533
|
+
}
|
|
534
|
+
state.lastConnectError = undefined;
|
|
535
|
+
this.markUsed(server);
|
|
536
|
+
await this.enforceMaxConcurrent(server);
|
|
515
537
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
state.
|
|
538
|
+
catch (error) {
|
|
539
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
540
|
+
state.lastConnectError = {
|
|
541
|
+
error: normalizedError,
|
|
542
|
+
timestamp: Date.now()
|
|
543
|
+
};
|
|
544
|
+
throw normalizedError;
|
|
519
545
|
}
|
|
520
|
-
this.markUsed(server);
|
|
521
|
-
await this.enforceMaxConcurrent(server);
|
|
522
546
|
})();
|
|
523
547
|
try {
|
|
524
548
|
await state.initPromise;
|
|
@@ -581,6 +605,9 @@ export class McpRouter {
|
|
|
581
605
|
state.idleTimer.unref();
|
|
582
606
|
}
|
|
583
607
|
}
|
|
608
|
+
nextRequestId() {
|
|
609
|
+
return nextRequestId(this.requestIdState);
|
|
610
|
+
}
|
|
584
611
|
createTransport(serverName, serverConfig) {
|
|
585
612
|
const onReconnected = async () => {
|
|
586
613
|
const state = this.states.get(serverName);
|
|
@@ -594,13 +621,13 @@ export class McpRouter {
|
|
|
594
621
|
this.resultCache?.invalidate(`${serverName}:`);
|
|
595
622
|
};
|
|
596
623
|
if (serverConfig.transport === "sse") {
|
|
597
|
-
return new this.transportRefs.sse(serverConfig, this.clientConfig, this.logger, onReconnected, this.tokenManager);
|
|
624
|
+
return new this.transportRefs.sse(serverConfig, this.clientConfig, this.logger, onReconnected, this.tokenManager, () => this.nextRequestId());
|
|
598
625
|
}
|
|
599
626
|
if (serverConfig.transport === "stdio") {
|
|
600
|
-
return new this.transportRefs.stdio(serverConfig, this.clientConfig, this.logger, onReconnected);
|
|
627
|
+
return new this.transportRefs.stdio(serverConfig, this.clientConfig, this.logger, onReconnected, () => this.nextRequestId());
|
|
601
628
|
}
|
|
602
629
|
if (serverConfig.transport === "streamable-http") {
|
|
603
|
-
return new this.transportRefs.streamableHttp(serverConfig, this.clientConfig, this.logger, onReconnected, this.tokenManager);
|
|
630
|
+
return new this.transportRefs.streamableHttp(serverConfig, this.clientConfig, this.logger, onReconnected, this.tokenManager, () => this.nextRequestId());
|
|
604
631
|
}
|
|
605
632
|
throw new Error(`Unsupported transport: ${serverConfig.transport}`);
|
|
606
633
|
}
|
package/dist/src/security.js
CHANGED
|
@@ -3,28 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Pipeline order: truncate → sanitize → trust-tag
|
|
5
5
|
*/
|
|
6
|
-
// Prompt injection
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/override\s+(all\s+)?instructions/gi,
|
|
6
|
+
// Prompt injection pattern sources to strip (compiled per call to avoid RegExp state leakage)
|
|
7
|
+
const INJECTION_PATTERN_SOURCES = [
|
|
8
|
+
"ignore\\s+(all\\s+)?previous\\s+instructions",
|
|
9
|
+
"ignore\\s+(all\\s+)?prior\\s+instructions",
|
|
10
|
+
"disregard\\s+(all\\s+)?previous\\s+instructions",
|
|
11
|
+
"you\\s+are\\s+now\\b",
|
|
12
|
+
"\\bact\\s+as\\s+(a|an)\\s+",
|
|
13
|
+
"pretend\\s+you\\s+are\\b",
|
|
14
|
+
"from\\s+now\\s+on\\s+you\\s+are\\b",
|
|
15
|
+
"new\\s+instructions\\s*:",
|
|
16
|
+
"override\\s+(all\\s+)?instructions",
|
|
18
17
|
];
|
|
18
|
+
const INJECTION_PATTERN_MULTILINE_SOURCES = ["^system\\s*:"];
|
|
19
19
|
function stripHtmlTags(text) {
|
|
20
20
|
return text.replace(/<[^>]*>/g, "");
|
|
21
21
|
}
|
|
22
22
|
function stripInjectionPatterns(text) {
|
|
23
23
|
let result = text;
|
|
24
|
-
for (const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
for (const source of INJECTION_PATTERN_SOURCES) {
|
|
25
|
+
result = result.replace(new RegExp(source, "gi"), "");
|
|
26
|
+
}
|
|
27
|
+
for (const source of INJECTION_PATTERN_MULTILINE_SOURCES) {
|
|
28
|
+
result = result.replace(new RegExp(source, "gim"), "");
|
|
28
29
|
}
|
|
29
30
|
return result;
|
|
30
31
|
}
|
|
@@ -11,6 +11,7 @@ export declare class StandaloneServer {
|
|
|
11
11
|
private initialized;
|
|
12
12
|
private lspMode;
|
|
13
13
|
private readonly tokenManager;
|
|
14
|
+
private readonly requestIdState;
|
|
14
15
|
private directTools;
|
|
15
16
|
private directConnections;
|
|
16
17
|
constructor(config: BridgeConfig, logger: Logger);
|
|
@@ -29,6 +30,7 @@ export declare class StandaloneServer {
|
|
|
29
30
|
/** Connect to all backend servers and discover their tools (direct mode). */
|
|
30
31
|
private discoverDirectTools;
|
|
31
32
|
private _doDiscovery;
|
|
33
|
+
private nextRequestId;
|
|
32
34
|
private createTransport;
|
|
33
35
|
/** Graceful shutdown: disconnect all backend servers. */
|
|
34
36
|
shutdown(): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpRouter } from "./mcp-router.js";
|
|
2
2
|
import { fetchToolsList, initializeProtocol, PACKAGE_VERSION } from "./protocol.js";
|
|
3
3
|
import { pickRegisteredToolName } from "./tool-naming.js";
|
|
4
|
+
import { nextRequestId, } from "./types.js";
|
|
4
5
|
import { SseTransport } from "./transport-sse.js";
|
|
5
6
|
import { StdioTransport } from "./transport-stdio.js";
|
|
6
7
|
import { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
@@ -17,6 +18,7 @@ export class StandaloneServer {
|
|
|
17
18
|
initialized = false;
|
|
18
19
|
lspMode = false;
|
|
19
20
|
tokenManager;
|
|
21
|
+
requestIdState = { value: 0 };
|
|
20
22
|
// Direct mode state
|
|
21
23
|
directTools = [];
|
|
22
24
|
directConnections = new Map();
|
|
@@ -36,25 +38,24 @@ export class StandaloneServer {
|
|
|
36
38
|
async startStdio() {
|
|
37
39
|
const stdin = process.stdin;
|
|
38
40
|
const stdout = process.stdout;
|
|
39
|
-
|
|
40
|
-
let buffer = "";
|
|
41
|
+
let buffer = Buffer.alloc(0);
|
|
41
42
|
// LSP framing state
|
|
42
43
|
let lspContentLength = -1; // -1 means not in LSP mode for current message
|
|
43
44
|
let lspHeadersDone = false;
|
|
44
45
|
stdin.on("data", (chunk) => {
|
|
45
|
-
|
|
46
|
+
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
|
|
47
|
+
buffer = Buffer.concat([buffer, chunkBuffer]);
|
|
46
48
|
// Process buffer in a loop — it may contain multiple messages
|
|
47
49
|
let progress = true;
|
|
48
50
|
while (progress) {
|
|
49
51
|
progress = false;
|
|
50
52
|
// If we're reading an LSP body, check if we have enough bytes
|
|
51
53
|
if (lspContentLength >= 0 && lspHeadersDone) {
|
|
52
|
-
|
|
53
|
-
if (bufferBytes >= lspContentLength) {
|
|
54
|
+
if (buffer.length >= lspContentLength) {
|
|
54
55
|
// Extract exactly lspContentLength bytes (LSP spec defines Content-Length in bytes)
|
|
55
|
-
const bodyBuffer =
|
|
56
|
+
const bodyBuffer = buffer.subarray(0, lspContentLength);
|
|
56
57
|
const body = bodyBuffer.toString("utf8");
|
|
57
|
-
buffer = buffer.
|
|
58
|
+
buffer = buffer.subarray(lspContentLength);
|
|
58
59
|
lspContentLength = -1;
|
|
59
60
|
lspHeadersDone = false;
|
|
60
61
|
const trimmed = body.trim();
|
|
@@ -68,15 +69,16 @@ export class StandaloneServer {
|
|
|
68
69
|
break;
|
|
69
70
|
}
|
|
70
71
|
// Look for complete lines to detect framing
|
|
71
|
-
const newlineIdx = buffer.indexOf(
|
|
72
|
+
const newlineIdx = buffer.indexOf(0x0a);
|
|
72
73
|
if (newlineIdx === -1)
|
|
73
74
|
break;
|
|
74
|
-
const
|
|
75
|
+
const lineBuffer = buffer.subarray(0, newlineIdx);
|
|
76
|
+
const line = lineBuffer.toString("utf8").replace(/\r$/, "");
|
|
75
77
|
const trimmed = line.trim();
|
|
76
78
|
// LSP header detection
|
|
77
79
|
if (lspContentLength >= 0 && !lspHeadersDone) {
|
|
78
80
|
// We're reading LSP headers — consume until empty line
|
|
79
|
-
buffer = buffer.
|
|
81
|
+
buffer = buffer.subarray(newlineIdx + 1);
|
|
80
82
|
progress = true;
|
|
81
83
|
if (trimmed === "") {
|
|
82
84
|
// End of headers — next read the body
|
|
@@ -93,13 +95,13 @@ export class StandaloneServer {
|
|
|
93
95
|
if (!isNaN(length) && length > 0) {
|
|
94
96
|
lspContentLength = length;
|
|
95
97
|
lspHeadersDone = false;
|
|
96
|
-
buffer = buffer.
|
|
98
|
+
buffer = buffer.subarray(newlineIdx + 1);
|
|
97
99
|
progress = true;
|
|
98
100
|
continue;
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
// Newline-delimited JSON: consume the line
|
|
102
|
-
buffer = buffer.
|
|
104
|
+
buffer = buffer.subarray(newlineIdx + 1);
|
|
103
105
|
progress = true;
|
|
104
106
|
if (!trimmed || !trimmed.startsWith("{"))
|
|
105
107
|
continue;
|
|
@@ -396,6 +398,9 @@ export class StandaloneServer {
|
|
|
396
398
|
}
|
|
397
399
|
}
|
|
398
400
|
}
|
|
401
|
+
nextRequestId() {
|
|
402
|
+
return nextRequestId(this.requestIdState);
|
|
403
|
+
}
|
|
399
404
|
createTransport(serverName, serverConfig) {
|
|
400
405
|
const onReconnected = async () => {
|
|
401
406
|
this.logger.info(`[mcp-bridge] ${serverName} reconnected, refreshing tools`);
|
|
@@ -403,11 +408,11 @@ export class StandaloneServer {
|
|
|
403
408
|
};
|
|
404
409
|
switch (serverConfig.transport) {
|
|
405
410
|
case "sse":
|
|
406
|
-
return new SseTransport(serverConfig, this.config, this.logger, onReconnected, this.tokenManager);
|
|
411
|
+
return new SseTransport(serverConfig, this.config, this.logger, onReconnected, this.tokenManager, () => this.nextRequestId());
|
|
407
412
|
case "stdio":
|
|
408
|
-
return new StdioTransport(serverConfig, this.config, this.logger, onReconnected);
|
|
413
|
+
return new StdioTransport(serverConfig, this.config, this.logger, onReconnected, () => this.nextRequestId());
|
|
409
414
|
case "streamable-http":
|
|
410
|
-
return new StreamableHttpTransport(serverConfig, this.config, this.logger, onReconnected, this.tokenManager);
|
|
415
|
+
return new StreamableHttpTransport(serverConfig, this.config, this.logger, onReconnected, this.tokenManager, () => this.nextRequestId());
|
|
411
416
|
default:
|
|
412
417
|
throw new Error(`Unsupported transport: ${serverConfig.transport}`);
|
|
413
418
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { McpTransport, McpRequest, McpResponse, McpServerConfig, McpClientConfig, Logger, JsonRpcMessage } from "./types.js";
|
|
1
|
+
import { McpTransport, McpRequest, McpResponse, McpServerConfig, McpClientConfig, Logger, JsonRpcMessage, RequestIdGenerator } from "./types.js";
|
|
2
2
|
import type { OAuth2Config, OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
3
3
|
export type PendingRequest = {
|
|
4
4
|
resolve: (value: McpResponse) => void;
|
|
@@ -22,12 +22,15 @@ export declare abstract class BaseTransport implements McpTransport {
|
|
|
22
22
|
protected reconnectTimer: NodeJS.Timeout | null;
|
|
23
23
|
protected onReconnected?: () => Promise<void>;
|
|
24
24
|
protected backoffDelay: number;
|
|
25
|
-
|
|
25
|
+
private readonly requestIdState;
|
|
26
|
+
private readonly requestIdGenerator;
|
|
27
|
+
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, requestIdGenerator?: RequestIdGenerator);
|
|
26
28
|
abstract connect(): Promise<void>;
|
|
27
29
|
abstract disconnect(): Promise<void>;
|
|
28
30
|
abstract sendRequest(request: McpRequest): Promise<McpResponse>;
|
|
29
31
|
abstract sendNotification(notification: any): Promise<void>;
|
|
30
32
|
isConnected(): boolean;
|
|
33
|
+
protected nextRequestId(): number;
|
|
31
34
|
/** Human-readable transport name for log messages (e.g. "stdio", "SSE", "streamable-http"). */
|
|
32
35
|
protected abstract get transportName(): string;
|
|
33
36
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nextRequestId } from "./types.js";
|
|
1
2
|
import { loadOpenClawDotEnvFallback } from "./config.js";
|
|
2
3
|
/**
|
|
3
4
|
* Base class for all MCP transports. Provides shared logic for:
|
|
@@ -16,15 +17,21 @@ export class BaseTransport {
|
|
|
16
17
|
reconnectTimer = null;
|
|
17
18
|
onReconnected;
|
|
18
19
|
backoffDelay = 0;
|
|
19
|
-
|
|
20
|
+
requestIdState = { value: 0 };
|
|
21
|
+
requestIdGenerator;
|
|
22
|
+
constructor(config, clientConfig, logger, onReconnected, requestIdGenerator) {
|
|
20
23
|
this.config = config;
|
|
21
24
|
this.clientConfig = clientConfig;
|
|
22
25
|
this.logger = logger;
|
|
23
26
|
this.onReconnected = onReconnected;
|
|
27
|
+
this.requestIdGenerator = requestIdGenerator ?? (() => nextRequestId(this.requestIdState));
|
|
24
28
|
}
|
|
25
29
|
isConnected() {
|
|
26
30
|
return this.connected;
|
|
27
31
|
}
|
|
32
|
+
nextRequestId() {
|
|
33
|
+
return this.requestIdGenerator();
|
|
34
|
+
}
|
|
28
35
|
/**
|
|
29
36
|
* Route an incoming JSON-RPC message to the appropriate handler:
|
|
30
37
|
* - notifications/tools/list_changed -> trigger tool refresh
|
|
@@ -130,7 +137,7 @@ export function resolveEnvVars(value, contextDescription, extraEnv, envFallback)
|
|
|
130
137
|
return fallbackVal;
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
|
-
if (resolved === undefined) {
|
|
140
|
+
if (resolved === undefined || resolved === "") {
|
|
134
141
|
throw new Error(`[mcp-bridge] Missing required environment variable "${varName}" while resolving ${contextDescription}`);
|
|
135
142
|
}
|
|
136
143
|
return resolved;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger, McpClientConfig, McpRequest, McpResponse, McpServerConfig } from "./types.js";
|
|
1
|
+
import { Logger, McpClientConfig, McpRequest, McpResponse, McpServerConfig, RequestIdGenerator } from "./types.js";
|
|
2
2
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
3
3
|
import { BaseTransport } from "./transport-base.js";
|
|
4
4
|
export declare class SseTransport extends BaseTransport {
|
|
@@ -8,7 +8,7 @@ export declare class SseTransport extends BaseTransport {
|
|
|
8
8
|
private pendingRequestControllers;
|
|
9
9
|
private readonly tokenManager;
|
|
10
10
|
protected get transportName(): string;
|
|
11
|
-
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager);
|
|
11
|
+
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: RequestIdGenerator);
|
|
12
12
|
connect(): Promise<void>;
|
|
13
13
|
private _onEndpointReceived;
|
|
14
14
|
private getBaseHeaders;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { nextRequestId } from "./types.js";
|
|
2
1
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
3
2
|
import { BaseTransport, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
|
|
4
3
|
export class SseTransport extends BaseTransport {
|
|
@@ -8,8 +7,8 @@ export class SseTransport extends BaseTransport {
|
|
|
8
7
|
pendingRequestControllers = new Map();
|
|
9
8
|
tokenManager;
|
|
10
9
|
get transportName() { return "SSE"; }
|
|
11
|
-
constructor(config, clientConfig, logger, onReconnected, tokenManager) {
|
|
12
|
-
super(config, clientConfig, logger, onReconnected);
|
|
10
|
+
constructor(config, clientConfig, logger, onReconnected, tokenManager, requestIdGenerator) {
|
|
11
|
+
super(config, clientConfig, logger, onReconnected, requestIdGenerator);
|
|
13
12
|
this.tokenManager = tokenManager ?? new OAuth2TokenManager(logger);
|
|
14
13
|
}
|
|
15
14
|
async connect() {
|
|
@@ -193,7 +192,7 @@ export class SseTransport extends BaseTransport {
|
|
|
193
192
|
if (!this.connected || !this.endpointUrl) {
|
|
194
193
|
throw new Error("SSE transport not connected or no endpoint URL");
|
|
195
194
|
}
|
|
196
|
-
const id = nextRequestId();
|
|
195
|
+
const id = this.nextRequestId();
|
|
197
196
|
const requestWithId = { ...request, id };
|
|
198
197
|
return new Promise((resolve, reject) => {
|
|
199
198
|
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { nextRequestId } from "./types.js";
|
|
3
2
|
import { BaseTransport, resolveEnvRecord, resolveArgs } from "./transport-base.js";
|
|
4
3
|
export class StdioTransport extends BaseTransport {
|
|
5
4
|
process = null;
|
|
@@ -130,8 +129,15 @@ export class StdioTransport extends BaseTransport {
|
|
|
130
129
|
this.process.once("error", onProcessError);
|
|
131
130
|
this.process.once("exit", onProcessExit);
|
|
132
131
|
timeout = setTimeout(() => {
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
const timeoutError = new Error(`Stdio process startup timeout: no data received within ${connectionTimeout}ms`);
|
|
133
|
+
this.logger.warn(`[mcp-bridge] ${timeoutError.message}; terminating unresponsive process`);
|
|
134
|
+
try {
|
|
135
|
+
this.process?.kill("SIGTERM");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Ignore kill errors and reject with timeout
|
|
139
|
+
}
|
|
140
|
+
settleReject(timeoutError);
|
|
135
141
|
}, connectionTimeout);
|
|
136
142
|
});
|
|
137
143
|
}
|
|
@@ -156,7 +162,7 @@ export class StdioTransport extends BaseTransport {
|
|
|
156
162
|
if (!this.connected || !this.process?.stdin) {
|
|
157
163
|
throw new Error("Stdio transport not connected");
|
|
158
164
|
}
|
|
159
|
-
const id = nextRequestId();
|
|
165
|
+
const id = this.nextRequestId();
|
|
160
166
|
const requestWithId = { ...request, id };
|
|
161
167
|
return new Promise((resolve, reject) => {
|
|
162
168
|
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger, McpClientConfig, McpRequest, McpResponse, McpServerConfig } from "./types.js";
|
|
1
|
+
import { Logger, McpClientConfig, McpRequest, McpResponse, McpServerConfig, RequestIdGenerator } from "./types.js";
|
|
2
2
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
3
3
|
import { BaseTransport } from "./transport-base.js";
|
|
4
4
|
export declare class StreamableHttpTransport extends BaseTransport {
|
|
@@ -7,7 +7,7 @@ export declare class StreamableHttpTransport extends BaseTransport {
|
|
|
7
7
|
private pendingRequestControllers;
|
|
8
8
|
private readonly tokenManager;
|
|
9
9
|
protected get transportName(): string;
|
|
10
|
-
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager);
|
|
10
|
+
constructor(config: McpServerConfig, clientConfig: McpClientConfig, logger: Logger, onReconnected?: () => Promise<void>, tokenManager?: OAuth2TokenManager, requestIdGenerator?: RequestIdGenerator);
|
|
11
11
|
connect(): Promise<void>;
|
|
12
12
|
private getBaseHeaders;
|
|
13
13
|
private refreshResolvedHeaders;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { nextRequestId } from "./types.js";
|
|
2
1
|
import { OAuth2TokenManager } from "./oauth2-token-manager.js";
|
|
3
2
|
import { BaseTransport, resolveOAuth2Config, resolveServerHeaders, resolveServerHeadersAsync, warnIfNonTlsRemoteUrl, } from "./transport-base.js";
|
|
4
3
|
export class StreamableHttpTransport extends BaseTransport {
|
|
@@ -7,8 +6,8 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
7
6
|
pendingRequestControllers = new Map();
|
|
8
7
|
tokenManager;
|
|
9
8
|
get transportName() { return "streamable-http"; }
|
|
10
|
-
constructor(config, clientConfig, logger, onReconnected, tokenManager) {
|
|
11
|
-
super(config, clientConfig, logger, onReconnected);
|
|
9
|
+
constructor(config, clientConfig, logger, onReconnected, tokenManager, requestIdGenerator) {
|
|
10
|
+
super(config, clientConfig, logger, onReconnected, requestIdGenerator);
|
|
12
11
|
this.tokenManager = tokenManager ?? new OAuth2TokenManager(logger);
|
|
13
12
|
}
|
|
14
13
|
async connect() {
|
|
@@ -68,7 +67,7 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
68
67
|
if (!this.connected || !this.config.url) {
|
|
69
68
|
throw new Error("Streamable HTTP transport not connected");
|
|
70
69
|
}
|
|
71
|
-
const id = nextRequestId();
|
|
70
|
+
const id = this.nextRequestId();
|
|
72
71
|
const requestWithId = { ...request, id };
|
|
73
72
|
return new Promise((resolve, reject) => {
|
|
74
73
|
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ export interface McpClientConfig {
|
|
|
52
52
|
requestTimeoutMs?: number;
|
|
53
53
|
shutdownTimeoutMs?: number;
|
|
54
54
|
routerIdleTimeoutMs?: number;
|
|
55
|
+
routerConnectErrorCooldownMs?: number;
|
|
55
56
|
routerMaxConcurrent?: number;
|
|
56
57
|
maxBatchSize?: number;
|
|
57
58
|
schemaCompression?: {
|
|
@@ -108,7 +109,11 @@ export interface McpRequest {
|
|
|
108
109
|
export interface McpCallRequest extends McpRequest {
|
|
109
110
|
id: number;
|
|
110
111
|
}
|
|
111
|
-
export
|
|
112
|
+
export interface RequestIdState {
|
|
113
|
+
value: number;
|
|
114
|
+
}
|
|
115
|
+
export type RequestIdGenerator = () => number;
|
|
116
|
+
export declare function nextRequestId(state: RequestIdState): number;
|
|
112
117
|
export interface McpResponse {
|
|
113
118
|
jsonrpc: "2.0";
|
|
114
119
|
id: number;
|
package/dist/src/types.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
globalRequestId = 1;
|
|
1
|
+
export function nextRequestId(state) {
|
|
2
|
+
state.value++;
|
|
3
|
+
if (state.value >= Number.MAX_SAFE_INTEGER) {
|
|
4
|
+
state.value = 1;
|
|
6
5
|
}
|
|
7
|
-
return
|
|
6
|
+
return state.value;
|
|
8
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwerk/mcp-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Standalone MCP server that multiplexes multiple MCP servers into one interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"homepage": "https://github.com/AIWerk/mcp-bridge#readme",
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/AIWerk/mcp-bridge"
|
|
29
|
+
"url": "git+https://github.com/AIWerk/mcp-bridge.git"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"mcp",
|