@aiwerk/mcp-bridge 1.9.0 → 2.0.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/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/mcp-router.d.ts +5 -0
- package/dist/src/mcp-router.js +102 -9
- package/dist/src/standalone-server.js +1 -1
- package/dist/src/tool-resolution.d.ts +1 -0
- package/dist/src/tool-resolution.js +5 -0
- package/dist/src/transport-base.d.ts +8 -0
- package/dist/src/transport-base.js +20 -0
- package/dist/src/transport-sse.d.ts +2 -0
- package/dist/src/transport-sse.js +21 -6
- package/dist/src/transport-stdio.d.ts +1 -0
- package/dist/src/transport-stdio.js +20 -8
- package/dist/src/transport-streamable-http.d.ts +3 -0
- package/dist/src/transport-streamable-http.js +32 -14
- package/dist/src/types.d.ts +18 -0
- package/package.json +1 -1
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { BaseTransport, resolveEnvVars, resolveEnvRecord, resolveArgs, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
1
|
+
export { BaseTransport, resolveEnvVars, resolveEnvRecord, resolveArgs, resolveAuthHeaders, resolveServerHeaders, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
2
2
|
export { StdioTransport } from "./transport-stdio.js";
|
|
3
3
|
export { SseTransport } from "./transport-sse.js";
|
|
4
4
|
export { StreamableHttpTransport } from "./transport-streamable-http.js";
|
|
@@ -11,7 +11,7 @@ export type { ToolResolutionResult, ToolResolutionCandidate } from "./tool-resol
|
|
|
11
11
|
export { convertJsonSchemaToTypeBox, createToolParameters, setTypeBoxLoader, setSchemaLogger } from "./schema-convert.js";
|
|
12
12
|
export { initializeProtocol, fetchToolsList, PACKAGE_VERSION } from "./protocol.js";
|
|
13
13
|
export { loadConfig, parseEnvFile, initConfigDir, getConfigDir } from "./config.js";
|
|
14
|
-
export type { Logger, McpServerConfig, McpClientConfig, McpTool, McpRequest, McpCallRequest, McpResponse, JsonRpcMessage, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
|
|
14
|
+
export type { Logger, McpServerConfig, McpClientConfig, HttpAuthConfig, RetryConfig, McpTool, McpRequest, McpCallRequest, McpResponse, JsonRpcMessage, McpTransport, McpServerConnection, BridgeConfig, } from "./types.js";
|
|
15
15
|
export { nextRequestId } from "./types.js";
|
|
16
16
|
export { pickRegisteredToolName } from "./tool-naming.js";
|
|
17
17
|
export { StandaloneServer } from "./standalone-server.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Core exports for @aiwerk/mcp-bridge
|
|
2
2
|
// Transport classes
|
|
3
|
-
export { BaseTransport, resolveEnvVars, resolveEnvRecord, resolveArgs, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
3
|
+
export { BaseTransport, resolveEnvVars, resolveEnvRecord, resolveArgs, resolveAuthHeaders, resolveServerHeaders, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
4
4
|
export { StdioTransport } from "./transport-stdio.js";
|
|
5
5
|
export { SseTransport } from "./transport-sse.js";
|
|
6
6
|
export { StreamableHttpTransport } from "./transport-streamable-http.js";
|
package/dist/src/mcp-router.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export type RouterDispatchResponse = {
|
|
|
37
37
|
action: "call";
|
|
38
38
|
tool: string;
|
|
39
39
|
result: any;
|
|
40
|
+
retries?: number;
|
|
40
41
|
} | {
|
|
41
42
|
server: string;
|
|
42
43
|
action: "schema";
|
|
@@ -122,7 +123,11 @@ export declare class McpRouter {
|
|
|
122
123
|
inputSchema: any;
|
|
123
124
|
}>;
|
|
124
125
|
private getPromotionStats;
|
|
126
|
+
private getRetryPolicy;
|
|
127
|
+
private classifyTransientError;
|
|
128
|
+
private callToolWithRetry;
|
|
125
129
|
disconnectAll(): Promise<void>;
|
|
130
|
+
shutdown(timeoutMs?: number): Promise<void>;
|
|
126
131
|
private ensureConnected;
|
|
127
132
|
private enforceMaxConcurrent;
|
|
128
133
|
private disconnectServer;
|
package/dist/src/mcp-router.js
CHANGED
|
@@ -12,6 +12,7 @@ import { ToolResolver } from "./tool-resolution.js";
|
|
|
12
12
|
const DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
13
13
|
const DEFAULT_MAX_CONCURRENT = 5;
|
|
14
14
|
const DEFAULT_MAX_BATCH_SIZE = 10;
|
|
15
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
15
16
|
export class McpRouter {
|
|
16
17
|
servers;
|
|
17
18
|
clientConfig;
|
|
@@ -227,14 +228,8 @@ export class McpRouter {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
this.markUsed(server);
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
method: "tools/call",
|
|
233
|
-
params: {
|
|
234
|
-
name: tool,
|
|
235
|
-
arguments: params ?? {}
|
|
236
|
-
}
|
|
237
|
-
});
|
|
231
|
+
const callOutcome = await this.callToolWithRetry(server, tool, params ?? {}, state.transport);
|
|
232
|
+
const response = callOutcome.response;
|
|
238
233
|
if (response.error) {
|
|
239
234
|
return this.error("mcp_error", response.error.message, undefined, response.error.code);
|
|
240
235
|
}
|
|
@@ -248,7 +243,13 @@ export class McpRouter {
|
|
|
248
243
|
if (this.resultCache && cacheKey) {
|
|
249
244
|
this.resultCache.set(cacheKey, result);
|
|
250
245
|
}
|
|
251
|
-
return {
|
|
246
|
+
return {
|
|
247
|
+
server,
|
|
248
|
+
action: "call",
|
|
249
|
+
tool,
|
|
250
|
+
result,
|
|
251
|
+
...(callOutcome.retries > 0 ? { retries: callOutcome.retries } : {})
|
|
252
|
+
};
|
|
252
253
|
}
|
|
253
254
|
catch (error) {
|
|
254
255
|
return this.error("mcp_error", error instanceof Error ? error.message : String(error));
|
|
@@ -390,11 +391,103 @@ export class McpRouter {
|
|
|
390
391
|
}));
|
|
391
392
|
return { action: "promotions", promoted, stats };
|
|
392
393
|
}
|
|
394
|
+
getRetryPolicy(server) {
|
|
395
|
+
const globalRetry = this.clientConfig.retry ?? {};
|
|
396
|
+
const serverRetry = this.servers[server].retry ?? {};
|
|
397
|
+
const maxAttemptsRaw = serverRetry.maxAttempts ?? globalRetry.maxAttempts ?? 1;
|
|
398
|
+
const delayMsRaw = serverRetry.delayMs ?? globalRetry.delayMs ?? 1000;
|
|
399
|
+
const backoffMultiplierRaw = serverRetry.backoffMultiplier ?? globalRetry.backoffMultiplier ?? 2;
|
|
400
|
+
const retryOn = serverRetry.retryOn ?? globalRetry.retryOn ?? ["timeout", "connection_error"];
|
|
401
|
+
return {
|
|
402
|
+
maxAttempts: Math.max(1, Math.floor(maxAttemptsRaw)),
|
|
403
|
+
delayMs: Math.max(0, Math.floor(delayMsRaw)),
|
|
404
|
+
backoffMultiplier: Math.max(1, backoffMultiplierRaw),
|
|
405
|
+
retryOn: new Set(retryOn)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
classifyTransientError(error) {
|
|
409
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
410
|
+
if (message.includes("timeout") ||
|
|
411
|
+
message.includes("timed out") ||
|
|
412
|
+
message.includes("abort")) {
|
|
413
|
+
return "timeout";
|
|
414
|
+
}
|
|
415
|
+
if (message.includes("connection") ||
|
|
416
|
+
message.includes("econnreset") ||
|
|
417
|
+
message.includes("socket hang up") ||
|
|
418
|
+
message.includes("network") ||
|
|
419
|
+
message.includes("fetch failed") ||
|
|
420
|
+
message.includes("econnrefused") ||
|
|
421
|
+
message.includes("enotfound")) {
|
|
422
|
+
return "connection_error";
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
async callToolWithRetry(server, tool, args, transport) {
|
|
427
|
+
const retryPolicy = this.getRetryPolicy(server);
|
|
428
|
+
let retries = 0;
|
|
429
|
+
let lastError;
|
|
430
|
+
for (let attempt = 0; attempt < retryPolicy.maxAttempts; attempt++) {
|
|
431
|
+
try {
|
|
432
|
+
const response = await transport.sendRequest({
|
|
433
|
+
jsonrpc: "2.0",
|
|
434
|
+
method: "tools/call",
|
|
435
|
+
params: {
|
|
436
|
+
name: tool,
|
|
437
|
+
arguments: args
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return { response, retries };
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
lastError = error;
|
|
444
|
+
const category = this.classifyTransientError(error);
|
|
445
|
+
const shouldRetry = category !== null &&
|
|
446
|
+
retryPolicy.retryOn.has(category) &&
|
|
447
|
+
attempt < retryPolicy.maxAttempts - 1;
|
|
448
|
+
if (!shouldRetry) {
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
retries += 1;
|
|
452
|
+
const delay = retryPolicy.delayMs * Math.pow(retryPolicy.backoffMultiplier, attempt);
|
|
453
|
+
if (delay > 0) {
|
|
454
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
459
|
+
}
|
|
393
460
|
async disconnectAll() {
|
|
394
461
|
for (const serverName of Object.keys(this.servers)) {
|
|
395
462
|
await this.disconnectServer(serverName);
|
|
396
463
|
}
|
|
397
464
|
}
|
|
465
|
+
async shutdown(timeoutMs = this.clientConfig.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS) {
|
|
466
|
+
const effectiveTimeout = Math.max(0, timeoutMs);
|
|
467
|
+
for (const [serverName, state] of this.states) {
|
|
468
|
+
if (state.idleTimer) {
|
|
469
|
+
clearTimeout(state.idleTimer);
|
|
470
|
+
state.idleTimer = null;
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
if (state.transport.shutdown) {
|
|
474
|
+
await state.transport.shutdown(effectiveTimeout);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
await state.transport.disconnect();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
this.logger.warn(`[mcp-bridge] Router shutdown: failed to close ${serverName}:`, error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
this.states.clear();
|
|
485
|
+
this.toolResolver.clear();
|
|
486
|
+
if (this.intentRouter) {
|
|
487
|
+
this.intentRouter.clearIndex();
|
|
488
|
+
}
|
|
489
|
+
this.resultCache?.invalidate();
|
|
490
|
+
}
|
|
398
491
|
async ensureConnected(server) {
|
|
399
492
|
let state = this.states.get(server);
|
|
400
493
|
if (!state) {
|
|
@@ -413,7 +413,7 @@ export class StandaloneServer {
|
|
|
413
413
|
async shutdown() {
|
|
414
414
|
this.logger.info("[mcp-bridge] Shutting down...");
|
|
415
415
|
if (this.router) {
|
|
416
|
-
await this.router.
|
|
416
|
+
await this.router.shutdown(this.config.shutdownTimeoutMs);
|
|
417
417
|
}
|
|
418
418
|
for (const [name, conn] of this.directConnections) {
|
|
419
419
|
try {
|
|
@@ -26,6 +26,7 @@ export declare class ToolResolver {
|
|
|
26
26
|
resolve(toolName: string, params?: Record<string, unknown>, serverHint?: string): ToolResolutionResult;
|
|
27
27
|
recordCall(server: string, tool: string): void;
|
|
28
28
|
getKnownToolNames(): string[];
|
|
29
|
+
clear(): void;
|
|
29
30
|
private scoreCandidate;
|
|
30
31
|
private wasUsedRecently;
|
|
31
32
|
private computeParamMatch;
|
|
@@ -99,6 +99,11 @@ export class ToolResolver {
|
|
|
99
99
|
getKnownToolNames() {
|
|
100
100
|
return [...this.toolsByName.keys()];
|
|
101
101
|
}
|
|
102
|
+
clear() {
|
|
103
|
+
this.toolsByName.clear();
|
|
104
|
+
this.toolNamesByServer.clear();
|
|
105
|
+
this.recentCalls.length = 0;
|
|
106
|
+
}
|
|
102
107
|
scoreCandidate(server, inputSchema, params) {
|
|
103
108
|
const base = this.basePriority.get(server) ?? BASE_PRIORITY_MIN;
|
|
104
109
|
const recency = this.wasUsedRecently(server) ? RECENCY_BOOST : 0;
|
|
@@ -70,6 +70,14 @@ export declare function resolveEnvRecord(record: Record<string, string>, context
|
|
|
70
70
|
* @param extraEnv - Additional env vars to check before process.env
|
|
71
71
|
*/
|
|
72
72
|
export declare function resolveArgs(args: string[], extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Resolve auth config into HTTP headers.
|
|
75
|
+
*/
|
|
76
|
+
export declare function resolveAuthHeaders(config: McpServerConfig, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): Record<string, string>;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve server headers and merge auth headers (auth takes precedence).
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveServerHeaders(config: McpServerConfig, extraEnv?: Record<string, string | undefined>, envFallback?: () => Record<string, string>): Record<string, string>;
|
|
73
81
|
/**
|
|
74
82
|
* Warn if a URL uses non-TLS HTTP to a remote (non-localhost) host.
|
|
75
83
|
*/
|
|
@@ -159,6 +159,26 @@ export function resolveEnvRecord(record, contextPrefix, extraEnv, envFallback) {
|
|
|
159
159
|
export function resolveArgs(args, extraEnv, envFallback) {
|
|
160
160
|
return args.map(arg => resolveEnvVars(arg, `arg "${arg}"`, extraEnv, envFallback));
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve auth config into HTTP headers.
|
|
164
|
+
*/
|
|
165
|
+
export function resolveAuthHeaders(config, extraEnv, envFallback) {
|
|
166
|
+
if (!config.auth)
|
|
167
|
+
return {};
|
|
168
|
+
if (config.auth.type === "bearer") {
|
|
169
|
+
const token = resolveEnvVars(config.auth.token, "auth token", extraEnv, envFallback);
|
|
170
|
+
return { Authorization: `Bearer ${token}` };
|
|
171
|
+
}
|
|
172
|
+
return resolveEnvRecord(config.auth.headers, "auth header", extraEnv, envFallback);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Resolve server headers and merge auth headers (auth takes precedence).
|
|
176
|
+
*/
|
|
177
|
+
export function resolveServerHeaders(config, extraEnv, envFallback) {
|
|
178
|
+
const base = resolveEnvRecord(config.headers || {}, "header", extraEnv, envFallback);
|
|
179
|
+
const auth = resolveAuthHeaders(config, extraEnv, envFallback);
|
|
180
|
+
return { ...base, ...auth };
|
|
181
|
+
}
|
|
162
182
|
/**
|
|
163
183
|
* Warn if a URL uses non-TLS HTTP to a remote (non-localhost) host.
|
|
164
184
|
*/
|
|
@@ -4,6 +4,7 @@ export declare class SseTransport extends BaseTransport {
|
|
|
4
4
|
private endpointUrl;
|
|
5
5
|
private sseAbortController;
|
|
6
6
|
private resolvedHeaders;
|
|
7
|
+
private pendingRequestControllers;
|
|
7
8
|
protected get transportName(): string;
|
|
8
9
|
connect(): Promise<void>;
|
|
9
10
|
private _onEndpointReceived;
|
|
@@ -13,4 +14,5 @@ export declare class SseTransport extends BaseTransport {
|
|
|
13
14
|
sendRequest(request: McpRequest): Promise<McpResponse>;
|
|
14
15
|
private isSameOrigin;
|
|
15
16
|
disconnect(): Promise<void>;
|
|
17
|
+
shutdown(): Promise<void>;
|
|
16
18
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { nextRequestId } from "./types.js";
|
|
2
|
-
import { BaseTransport,
|
|
2
|
+
import { BaseTransport, resolveServerHeaders, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
3
3
|
export class SseTransport extends BaseTransport {
|
|
4
4
|
endpointUrl = null;
|
|
5
5
|
sseAbortController = null;
|
|
6
6
|
resolvedHeaders = null;
|
|
7
|
+
pendingRequestControllers = new Map();
|
|
7
8
|
get transportName() { return "SSE"; }
|
|
8
9
|
async connect() {
|
|
9
10
|
if (!this.config.url) {
|
|
@@ -11,7 +12,7 @@ export class SseTransport extends BaseTransport {
|
|
|
11
12
|
}
|
|
12
13
|
warnIfNonTlsRemoteUrl(this.config.url, this.logger);
|
|
13
14
|
// Resolve headers once and cache for all subsequent requests
|
|
14
|
-
this.resolvedHeaders =
|
|
15
|
+
this.resolvedHeaders = resolveServerHeaders(this.config);
|
|
15
16
|
if (this.sseAbortController) {
|
|
16
17
|
this.sseAbortController.abort();
|
|
17
18
|
}
|
|
@@ -36,7 +37,7 @@ export class SseTransport extends BaseTransport {
|
|
|
36
37
|
async startEventStream() {
|
|
37
38
|
if (!this.config.url)
|
|
38
39
|
return;
|
|
39
|
-
const base = this.resolvedHeaders ??
|
|
40
|
+
const base = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
40
41
|
const headers = { ...base, "Accept": "text/event-stream" };
|
|
41
42
|
try {
|
|
42
43
|
const response = await fetch(this.config.url, {
|
|
@@ -130,7 +131,7 @@ export class SseTransport extends BaseTransport {
|
|
|
130
131
|
if (!this.connected || !this.endpointUrl) {
|
|
131
132
|
throw new Error("SSE transport not connected or no endpoint URL");
|
|
132
133
|
}
|
|
133
|
-
const base = this.resolvedHeaders ??
|
|
134
|
+
const base = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
134
135
|
const headers = { ...base, "Content-Type": "application/json" };
|
|
135
136
|
const response = await fetch(this.endpointUrl, {
|
|
136
137
|
method: "POST",
|
|
@@ -150,21 +151,27 @@ export class SseTransport extends BaseTransport {
|
|
|
150
151
|
return new Promise((resolve, reject) => {
|
|
151
152
|
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
|
152
153
|
const timeout = setTimeout(() => {
|
|
154
|
+
this.pendingRequestControllers.get(id)?.abort();
|
|
155
|
+
this.pendingRequestControllers.delete(id);
|
|
153
156
|
this.pendingRequests.delete(id);
|
|
154
157
|
reject(new Error(`Request timeout after ${requestTimeout}ms`));
|
|
155
158
|
}, requestTimeout);
|
|
156
159
|
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
157
|
-
const base = this.resolvedHeaders ??
|
|
160
|
+
const base = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
158
161
|
const headers = { ...base, "Content-Type": "application/json" };
|
|
162
|
+
const abortController = new AbortController();
|
|
163
|
+
this.pendingRequestControllers.set(id, abortController);
|
|
159
164
|
// The response arrives via the SSE stream (handleMessage), not from this fetch.
|
|
160
165
|
// The fetch only confirms the server accepted the request (HTTP 200).
|
|
161
166
|
// If the fetch fails, we reject immediately; otherwise we wait for the SSE stream.
|
|
162
167
|
fetch(this.endpointUrl, {
|
|
163
168
|
method: "POST",
|
|
164
169
|
headers,
|
|
165
|
-
body: JSON.stringify(requestWithId)
|
|
170
|
+
body: JSON.stringify(requestWithId),
|
|
171
|
+
signal: abortController.signal
|
|
166
172
|
})
|
|
167
173
|
.then((response) => {
|
|
174
|
+
this.pendingRequestControllers.delete(id);
|
|
168
175
|
if (!response.ok) {
|
|
169
176
|
clearTimeout(timeout);
|
|
170
177
|
this.pendingRequests.delete(id);
|
|
@@ -172,6 +179,7 @@ export class SseTransport extends BaseTransport {
|
|
|
172
179
|
}
|
|
173
180
|
})
|
|
174
181
|
.catch((error) => {
|
|
182
|
+
this.pendingRequestControllers.delete(id);
|
|
175
183
|
clearTimeout(timeout);
|
|
176
184
|
this.pendingRequests.delete(id);
|
|
177
185
|
reject(error);
|
|
@@ -197,6 +205,13 @@ export class SseTransport extends BaseTransport {
|
|
|
197
205
|
this.sseAbortController.abort();
|
|
198
206
|
this.sseAbortController = null;
|
|
199
207
|
}
|
|
208
|
+
for (const [, controller] of this.pendingRequestControllers) {
|
|
209
|
+
controller.abort();
|
|
210
|
+
}
|
|
211
|
+
this.pendingRequestControllers.clear();
|
|
200
212
|
this.rejectAllPending("Connection closed");
|
|
201
213
|
}
|
|
214
|
+
async shutdown() {
|
|
215
|
+
await this.disconnect();
|
|
216
|
+
}
|
|
202
217
|
}
|
|
@@ -15,6 +15,7 @@ export declare class StdioTransport extends BaseTransport {
|
|
|
15
15
|
private parseNewlineMessageFromBuffer;
|
|
16
16
|
private parseLspMessageFromBuffer;
|
|
17
17
|
disconnect(): Promise<void>;
|
|
18
|
+
shutdown(timeoutMs?: number): Promise<void>;
|
|
18
19
|
isConnected(): boolean;
|
|
19
20
|
private terminateProcessGracefully;
|
|
20
21
|
}
|
|
@@ -264,7 +264,20 @@ export class StdioTransport extends BaseTransport {
|
|
|
264
264
|
this.logger.debug("[mcp-bridge] Failed to send close notification during stdio disconnect");
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
|
-
await this.terminateProcessGracefully(activeProcess);
|
|
267
|
+
await this.terminateProcessGracefully(activeProcess, this.clientConfig.shutdownTimeoutMs ?? 5000);
|
|
268
|
+
if (this.process === activeProcess) {
|
|
269
|
+
this.process = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
this.rejectAllPending("Connection closed");
|
|
273
|
+
}
|
|
274
|
+
async shutdown(timeoutMs = this.clientConfig.shutdownTimeoutMs ?? 5000) {
|
|
275
|
+
this.isShuttingDown = true;
|
|
276
|
+
this.connected = false;
|
|
277
|
+
this.cleanupReconnectTimer();
|
|
278
|
+
const activeProcess = this.process;
|
|
279
|
+
if (activeProcess) {
|
|
280
|
+
await this.terminateProcessGracefully(activeProcess, timeoutMs);
|
|
268
281
|
if (this.process === activeProcess) {
|
|
269
282
|
this.process = null;
|
|
270
283
|
}
|
|
@@ -274,8 +287,8 @@ export class StdioTransport extends BaseTransport {
|
|
|
274
287
|
isConnected() {
|
|
275
288
|
return this.connected && this.process !== null;
|
|
276
289
|
}
|
|
277
|
-
async terminateProcessGracefully(proc) {
|
|
278
|
-
if (proc.exitCode !== null
|
|
290
|
+
async terminateProcessGracefully(proc, timeoutMs) {
|
|
291
|
+
if (proc.exitCode !== null)
|
|
279
292
|
return;
|
|
280
293
|
await new Promise((resolve) => {
|
|
281
294
|
let done = false;
|
|
@@ -292,21 +305,20 @@ export class StdioTransport extends BaseTransport {
|
|
|
292
305
|
const onExit = () => finish();
|
|
293
306
|
proc.once("exit", onExit);
|
|
294
307
|
try {
|
|
295
|
-
proc.kill("
|
|
308
|
+
proc.kill("SIGTERM");
|
|
296
309
|
}
|
|
297
310
|
catch {
|
|
298
311
|
finish();
|
|
299
312
|
return;
|
|
300
313
|
}
|
|
301
314
|
forceKillTimer = setTimeout(() => {
|
|
302
|
-
if (proc.exitCode === null
|
|
315
|
+
if (proc.exitCode === null) {
|
|
303
316
|
try {
|
|
304
|
-
proc.kill("
|
|
317
|
+
proc.kill("SIGKILL");
|
|
305
318
|
}
|
|
306
319
|
catch { /* ignore */ }
|
|
307
320
|
}
|
|
308
|
-
|
|
309
|
-
}, 2000);
|
|
321
|
+
}, Math.max(0, timeoutMs));
|
|
310
322
|
});
|
|
311
323
|
}
|
|
312
324
|
}
|
|
@@ -2,10 +2,13 @@ import { McpRequest, McpResponse } from "./types.js";
|
|
|
2
2
|
import { BaseTransport } from "./transport-base.js";
|
|
3
3
|
export declare class StreamableHttpTransport extends BaseTransport {
|
|
4
4
|
private sessionId?;
|
|
5
|
+
private resolvedHeaders;
|
|
6
|
+
private pendingRequestControllers;
|
|
5
7
|
protected get transportName(): string;
|
|
6
8
|
connect(): Promise<void>;
|
|
7
9
|
sendRequest(request: McpRequest): Promise<McpResponse>;
|
|
8
10
|
sendNotification(notification: any): Promise<void>;
|
|
9
11
|
private probeServer;
|
|
10
12
|
disconnect(): Promise<void>;
|
|
13
|
+
shutdown(): Promise<void>;
|
|
11
14
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { nextRequestId } from "./types.js";
|
|
2
|
-
import { BaseTransport,
|
|
2
|
+
import { BaseTransport, resolveServerHeaders, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
3
3
|
export class StreamableHttpTransport extends BaseTransport {
|
|
4
4
|
sessionId;
|
|
5
|
+
resolvedHeaders = null;
|
|
6
|
+
pendingRequestControllers = new Map();
|
|
5
7
|
get transportName() { return "streamable-http"; }
|
|
6
8
|
async connect() {
|
|
7
9
|
if (!this.config.url) {
|
|
8
10
|
throw new Error("Streamable HTTP transport requires URL");
|
|
9
11
|
}
|
|
10
12
|
warnIfNonTlsRemoteUrl(this.config.url, this.logger);
|
|
11
|
-
// Validate that all header env vars resolve (fail fast)
|
|
12
|
-
|
|
13
|
+
// Validate that all header/auth env vars resolve (fail fast)
|
|
14
|
+
this.resolvedHeaders = resolveServerHeaders(this.config);
|
|
13
15
|
await this.probeServer();
|
|
14
16
|
this.connected = true;
|
|
15
17
|
this.backoffDelay = this.clientConfig.reconnectIntervalMs || 30000;
|
|
@@ -23,25 +25,32 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
23
25
|
const requestWithId = { ...request, id };
|
|
24
26
|
return new Promise((resolve, reject) => {
|
|
25
27
|
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
|
28
|
+
const abortController = new AbortController();
|
|
26
29
|
const timeout = setTimeout(() => {
|
|
30
|
+
abortController.abort();
|
|
31
|
+
this.pendingRequestControllers.delete(id);
|
|
27
32
|
this.pendingRequests.delete(id);
|
|
28
33
|
reject(new Error(`Request timeout after ${requestTimeout}ms`));
|
|
29
34
|
}, requestTimeout);
|
|
30
35
|
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
31
|
-
|
|
36
|
+
this.pendingRequestControllers.set(id, abortController);
|
|
37
|
+
const base = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
38
|
+
const headers = {
|
|
39
|
+
...base,
|
|
32
40
|
"Accept": "application/json, text/event-stream",
|
|
33
|
-
...this.config.headers,
|
|
34
41
|
"Content-Type": "application/json"
|
|
35
|
-
}
|
|
42
|
+
};
|
|
36
43
|
if (this.sessionId) {
|
|
37
44
|
headers["mcp-session-id"] = this.sessionId;
|
|
38
45
|
}
|
|
39
46
|
fetch(this.config.url, {
|
|
40
47
|
method: "POST",
|
|
41
48
|
headers,
|
|
42
|
-
body: JSON.stringify(requestWithId)
|
|
49
|
+
body: JSON.stringify(requestWithId),
|
|
50
|
+
signal: abortController.signal
|
|
43
51
|
})
|
|
44
52
|
.then(async (response) => {
|
|
53
|
+
this.pendingRequestControllers.delete(id);
|
|
45
54
|
const responseSessionId = response.headers.get("mcp-session-id");
|
|
46
55
|
if (responseSessionId) {
|
|
47
56
|
this.sessionId = responseSessionId;
|
|
@@ -98,6 +107,7 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
98
107
|
}
|
|
99
108
|
})
|
|
100
109
|
.catch(error => {
|
|
110
|
+
this.pendingRequestControllers.delete(id);
|
|
101
111
|
clearTimeout(timeout);
|
|
102
112
|
this.pendingRequests.delete(id);
|
|
103
113
|
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
@@ -112,11 +122,12 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
112
122
|
if (!this.connected || !this.config.url) {
|
|
113
123
|
throw new Error("Streamable HTTP transport not connected");
|
|
114
124
|
}
|
|
115
|
-
const
|
|
125
|
+
const base = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
126
|
+
const headers = {
|
|
127
|
+
...base,
|
|
116
128
|
"Accept": "application/json, text/event-stream",
|
|
117
|
-
...this.config.headers,
|
|
118
129
|
"Content-Type": "application/json"
|
|
119
|
-
}
|
|
130
|
+
};
|
|
120
131
|
if (this.sessionId) {
|
|
121
132
|
headers["mcp-session-id"] = this.sessionId;
|
|
122
133
|
}
|
|
@@ -146,10 +157,10 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
146
157
|
if (!this.config.url)
|
|
147
158
|
return;
|
|
148
159
|
try {
|
|
149
|
-
const
|
|
160
|
+
const headers = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
161
|
+
const optionsResponse = await fetch(this.config.url, { method: "OPTIONS", headers });
|
|
150
162
|
if (optionsResponse.ok)
|
|
151
163
|
return;
|
|
152
|
-
const headers = resolveEnvRecord(this.config.headers || {}, "header");
|
|
153
164
|
const headResponse = await fetch(this.config.url, { method: "HEAD", headers });
|
|
154
165
|
if (!headResponse.ok) {
|
|
155
166
|
this.logger.warn(`[mcp-bridge] Streamable HTTP server probe: OPTIONS ${optionsResponse.status}, HEAD ${headResponse.status} (non-blocking, connection continues)`);
|
|
@@ -162,11 +173,15 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
162
173
|
async disconnect() {
|
|
163
174
|
this.connected = false;
|
|
164
175
|
this.cleanupReconnectTimer();
|
|
176
|
+
for (const [, controller] of this.pendingRequestControllers) {
|
|
177
|
+
controller.abort();
|
|
178
|
+
}
|
|
179
|
+
this.pendingRequestControllers.clear();
|
|
165
180
|
// Send DELETE request if we have a session to clean up
|
|
166
181
|
if (this.sessionId && this.config.url) {
|
|
167
182
|
try {
|
|
168
|
-
const
|
|
169
|
-
headers
|
|
183
|
+
const base = this.resolvedHeaders ?? resolveServerHeaders(this.config);
|
|
184
|
+
const headers = { ...base, "mcp-session-id": this.sessionId };
|
|
170
185
|
await fetch(this.config.url, {
|
|
171
186
|
method: "DELETE",
|
|
172
187
|
headers
|
|
@@ -180,4 +195,7 @@ export class StreamableHttpTransport extends BaseTransport {
|
|
|
180
195
|
}
|
|
181
196
|
this.rejectAllPending("Connection closed");
|
|
182
197
|
}
|
|
198
|
+
async shutdown() {
|
|
199
|
+
await this.disconnect();
|
|
200
|
+
}
|
|
183
201
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -4,12 +4,26 @@ export interface Logger {
|
|
|
4
4
|
error: (...args: unknown[]) => void;
|
|
5
5
|
debug: (...args: unknown[]) => void;
|
|
6
6
|
}
|
|
7
|
+
export type HttpAuthConfig = {
|
|
8
|
+
type: "bearer";
|
|
9
|
+
token: string;
|
|
10
|
+
} | {
|
|
11
|
+
type: "header";
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
export interface RetryConfig {
|
|
15
|
+
maxAttempts?: number;
|
|
16
|
+
delayMs?: number;
|
|
17
|
+
backoffMultiplier?: number;
|
|
18
|
+
retryOn?: Array<"timeout" | "connection_error">;
|
|
19
|
+
}
|
|
7
20
|
export interface McpServerConfig {
|
|
8
21
|
transport: "sse" | "stdio" | "streamable-http";
|
|
9
22
|
/** Human-readable description for router tool description generation */
|
|
10
23
|
description?: string;
|
|
11
24
|
url?: string;
|
|
12
25
|
headers?: Record<string, string>;
|
|
26
|
+
auth?: HttpAuthConfig;
|
|
13
27
|
command?: string;
|
|
14
28
|
args?: string[];
|
|
15
29
|
env?: Record<string, string>;
|
|
@@ -20,6 +34,7 @@ export interface McpServerConfig {
|
|
|
20
34
|
allow?: string[];
|
|
21
35
|
};
|
|
22
36
|
maxResultChars?: number;
|
|
37
|
+
retry?: RetryConfig;
|
|
23
38
|
}
|
|
24
39
|
export interface McpClientConfig {
|
|
25
40
|
servers: Record<string, McpServerConfig>;
|
|
@@ -28,6 +43,7 @@ export interface McpClientConfig {
|
|
|
28
43
|
reconnectIntervalMs?: number;
|
|
29
44
|
connectionTimeoutMs?: number;
|
|
30
45
|
requestTimeoutMs?: number;
|
|
46
|
+
shutdownTimeoutMs?: number;
|
|
31
47
|
routerIdleTimeoutMs?: number;
|
|
32
48
|
routerMaxConcurrent?: number;
|
|
33
49
|
maxBatchSize?: number;
|
|
@@ -49,6 +65,7 @@ export interface McpClientConfig {
|
|
|
49
65
|
minCalls?: number;
|
|
50
66
|
decayMs?: number;
|
|
51
67
|
};
|
|
68
|
+
retry?: RetryConfig;
|
|
52
69
|
resultCache?: {
|
|
53
70
|
enabled?: boolean;
|
|
54
71
|
maxEntries?: number;
|
|
@@ -98,6 +115,7 @@ export interface McpResponse {
|
|
|
98
115
|
export interface McpTransport {
|
|
99
116
|
connect(): Promise<void>;
|
|
100
117
|
disconnect(): Promise<void>;
|
|
118
|
+
shutdown?(timeoutMs?: number): Promise<void>;
|
|
101
119
|
sendRequest(request: McpRequest): Promise<McpResponse>;
|
|
102
120
|
sendNotification(notification: any): Promise<void>;
|
|
103
121
|
isConnected(): boolean;
|