@aiwerk/mcp-bridge 1.0.0 → 1.0.2
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/bin/mcp-bridge.d.ts +2 -0
- package/dist/bin/mcp-bridge.js +320 -0
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +145 -0
- package/{src/index.ts → dist/src/index.d.ts} +1 -30
- package/dist/src/index.js +21 -0
- package/dist/src/mcp-router.d.ts +65 -0
- package/dist/src/mcp-router.js +271 -0
- package/dist/src/protocol.d.ts +4 -0
- package/dist/src/protocol.js +58 -0
- package/dist/src/schema-convert.d.ts +11 -0
- package/dist/src/schema-convert.js +150 -0
- package/dist/src/standalone-server.d.ts +30 -0
- package/dist/src/standalone-server.js +312 -0
- package/dist/src/tool-naming.d.ts +3 -0
- package/dist/src/tool-naming.js +38 -0
- package/dist/src/transport-base.d.ts +76 -0
- package/dist/src/transport-base.js +163 -0
- package/dist/src/transport-sse.d.ts +16 -0
- package/dist/src/transport-sse.js +207 -0
- package/dist/src/transport-stdio.d.ts +20 -0
- package/dist/src/transport-stdio.js +281 -0
- package/dist/src/transport-streamable-http.d.ts +11 -0
- package/dist/src/transport-streamable-http.js +164 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +4 -0
- package/dist/src/update-checker.d.ts +25 -0
- package/dist/src/update-checker.js +132 -0
- package/package.json +19 -4
- package/scripts/install-server.ps1 +25 -58
- package/scripts/install-server.sh +37 -90
- package/servers/apify/README.md +6 -6
- package/servers/github/README.md +6 -6
- package/servers/google-maps/README.md +6 -6
- package/servers/hetzner/README.md +6 -6
- package/servers/hostinger/README.md +6 -6
- package/servers/linear/README.md +6 -6
- package/servers/miro/README.md +6 -6
- package/servers/notion/README.md +6 -6
- package/servers/stripe/README.md +6 -6
- package/servers/tavily/README.md +6 -6
- package/servers/todoist/README.md +6 -6
- package/servers/wise/README.md +6 -6
- package/bin/mcp-bridge.js +0 -9
- package/bin/mcp-bridge.ts +0 -335
- package/src/config.ts +0 -168
- package/src/mcp-router.ts +0 -366
- package/src/protocol.ts +0 -69
- package/src/schema-convert.ts +0 -178
- package/src/standalone-server.ts +0 -385
- package/src/tool-naming.ts +0 -51
- package/src/transport-base.ts +0 -199
- package/src/transport-sse.ts +0 -230
- package/src/transport-stdio.ts +0 -312
- package/src/transport-streamable-http.ts +0 -188
- package/src/types.ts +0 -88
- package/src/update-checker.ts +0 -155
- package/tests/collision.test.ts +0 -60
- package/tests/env-resolve.test.ts +0 -68
- package/tests/mcp-router.test.ts +0 -301
- package/tests/schema-convert.test.ts +0 -70
- package/tests/transport-base.test.ts +0 -214
- package/tsconfig.json +0 -15
package/src/transport-stdio.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import { spawn, ChildProcess } from "child_process";
|
|
2
|
-
import { McpRequest, McpResponse, McpServerConfig, nextRequestId } from "./types.js";
|
|
3
|
-
import { BaseTransport, resolveEnvRecord, resolveArgs } from "./transport-base.js";
|
|
4
|
-
|
|
5
|
-
export class StdioTransport extends BaseTransport {
|
|
6
|
-
private process: ChildProcess | null = null;
|
|
7
|
-
private framingMode: "auto" | "lsp" | "newline" = "auto";
|
|
8
|
-
private stdoutBuffer = Buffer.alloc(0);
|
|
9
|
-
private isShuttingDown = false;
|
|
10
|
-
|
|
11
|
-
protected get transportName(): string { return "stdio"; }
|
|
12
|
-
|
|
13
|
-
async connect(): Promise<void> {
|
|
14
|
-
if (!this.config.command) {
|
|
15
|
-
throw new Error("Stdio transport requires command");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
this.isShuttingDown = false;
|
|
20
|
-
await this.startProcess();
|
|
21
|
-
this.connected = true;
|
|
22
|
-
this.backoffDelay = this.clientConfig.reconnectIntervalMs || 30000;
|
|
23
|
-
} catch (error) {
|
|
24
|
-
this.logger.error("Stdio transport connection failed:", error);
|
|
25
|
-
throw error;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private async startProcess(): Promise<void> {
|
|
30
|
-
if (!this.config.command) return;
|
|
31
|
-
|
|
32
|
-
const env = { ...process.env, ...resolveEnvRecord(this.config.env || {}, "env key") };
|
|
33
|
-
const args = resolveArgs(this.config.args || [], env);
|
|
34
|
-
|
|
35
|
-
this.process = spawn(this.config.command, args, {
|
|
36
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
37
|
-
env
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
41
|
-
throw new Error("Failed to create process pipes");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.framingMode = this.config.framing || "auto";
|
|
45
|
-
this.stdoutBuffer = Buffer.alloc(0);
|
|
46
|
-
this.process.stdout.on("data", (data: Buffer) => {
|
|
47
|
-
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, data]);
|
|
48
|
-
// Safety limit: prevent unbounded buffer growth from misbehaving servers
|
|
49
|
-
const MAX_BUFFER = 50 * 1024 * 1024; // 50MB
|
|
50
|
-
if (this.stdoutBuffer.length > MAX_BUFFER) {
|
|
51
|
-
this.logger.error(`[mcp-bridge] Stdio buffer exceeded ${MAX_BUFFER} bytes, killing process`);
|
|
52
|
-
this.process?.kill();
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
this.processStdoutBuffer();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
this.process.stderr.on("data", (data: Buffer) => {
|
|
59
|
-
this.logger.debug(`MCP server stderr: ${data.toString()}`);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
this.process.on("exit", (code, signal) => {
|
|
63
|
-
this.logger.debug(`MCP server process exited: code=${code}, signal=${signal}`);
|
|
64
|
-
this.connected = false;
|
|
65
|
-
this.process = null;
|
|
66
|
-
this.rejectAllPending("Process exited");
|
|
67
|
-
|
|
68
|
-
if (!this.isShuttingDown) {
|
|
69
|
-
this.scheduleReconnect();
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
this.process.on("error", (error) => {
|
|
74
|
-
this.logger.error("MCP server process error:", error);
|
|
75
|
-
this.connected = false;
|
|
76
|
-
this.process = null;
|
|
77
|
-
this.rejectAllPending("Process error");
|
|
78
|
-
|
|
79
|
-
if (!this.isShuttingDown) {
|
|
80
|
-
this.scheduleReconnect();
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const connectionTimeout = this.clientConfig.connectionTimeoutMs || 5000;
|
|
85
|
-
await new Promise<void>((resolve, reject) => {
|
|
86
|
-
let settled = false;
|
|
87
|
-
let timeout: NodeJS.Timeout;
|
|
88
|
-
|
|
89
|
-
const cleanup = () => {
|
|
90
|
-
this.process?.stdout?.off("data", onFirstData);
|
|
91
|
-
this.process?.off("error", onProcessError);
|
|
92
|
-
this.process?.off("exit", onProcessExit);
|
|
93
|
-
clearTimeout(timeout);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const settleResolve = () => {
|
|
97
|
-
if (settled) return;
|
|
98
|
-
settled = true;
|
|
99
|
-
cleanup();
|
|
100
|
-
resolve();
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const settleReject = (error: Error) => {
|
|
104
|
-
if (settled) return;
|
|
105
|
-
settled = true;
|
|
106
|
-
cleanup();
|
|
107
|
-
reject(error);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const onFirstData = () => settleResolve();
|
|
111
|
-
const onProcessError = (error: Error) => settleReject(error);
|
|
112
|
-
const onProcessExit = () => settleReject(new Error("MCP server exited before stdout became ready"));
|
|
113
|
-
|
|
114
|
-
this.process!.stdout!.once("data", onFirstData);
|
|
115
|
-
this.process!.once("error", onProcessError);
|
|
116
|
-
this.process!.once("exit", onProcessExit);
|
|
117
|
-
|
|
118
|
-
timeout = setTimeout(() => {
|
|
119
|
-
this.logger.warn(`[mcp-bridge] Stdio startup stdout readiness timed out after ${connectionTimeout}ms; continuing`);
|
|
120
|
-
settleResolve();
|
|
121
|
-
}, connectionTimeout);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private writeMessage(message: any): void {
|
|
126
|
-
const json = JSON.stringify(message);
|
|
127
|
-
if (this.framingMode === "lsp") {
|
|
128
|
-
const body = Buffer.from(json, "utf8");
|
|
129
|
-
this.process!.stdin!.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
130
|
-
this.process!.stdin!.write(body);
|
|
131
|
-
} else {
|
|
132
|
-
this.process!.stdin!.write(json + '\n');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async sendNotification(notification: any): Promise<void> {
|
|
137
|
-
if (!this.connected || !this.process?.stdin) {
|
|
138
|
-
throw new Error("Stdio transport not connected");
|
|
139
|
-
}
|
|
140
|
-
this.writeMessage(notification);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async sendRequest(request: McpRequest): Promise<McpResponse> {
|
|
144
|
-
if (!this.connected || !this.process?.stdin) {
|
|
145
|
-
throw new Error("Stdio transport not connected");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const id = nextRequestId();
|
|
149
|
-
const requestWithId = { ...request, id };
|
|
150
|
-
|
|
151
|
-
return new Promise((resolve, reject) => {
|
|
152
|
-
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
|
153
|
-
const timeout = setTimeout(() => {
|
|
154
|
-
this.pendingRequests.delete(id);
|
|
155
|
-
reject(new Error(`Request timeout after ${requestTimeout}ms`));
|
|
156
|
-
}, requestTimeout);
|
|
157
|
-
|
|
158
|
-
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
this.writeMessage(requestWithId);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
clearTimeout(timeout);
|
|
164
|
-
this.pendingRequests.delete(id);
|
|
165
|
-
reject(error);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private processStdoutBuffer(): void {
|
|
171
|
-
while (true) {
|
|
172
|
-
if (this.framingMode === "auto") {
|
|
173
|
-
const bufferText = this.stdoutBuffer.toString("utf8");
|
|
174
|
-
if (bufferText.includes("Content-Length:")) {
|
|
175
|
-
this.framingMode = "lsp";
|
|
176
|
-
} else if (this.stdoutBuffer.includes(0x0a)) {
|
|
177
|
-
this.framingMode = "newline";
|
|
178
|
-
} else {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (this.framingMode === "lsp") {
|
|
184
|
-
if (!this.parseLspMessageFromBuffer()) return;
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!this.parseNewlineMessageFromBuffer()) return;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private parseNewlineMessageFromBuffer(): boolean {
|
|
193
|
-
const newlineIndex = this.stdoutBuffer.indexOf(0x0a);
|
|
194
|
-
if (newlineIndex === -1) return false;
|
|
195
|
-
|
|
196
|
-
const lineBuffer = this.stdoutBuffer.subarray(0, newlineIndex);
|
|
197
|
-
this.stdoutBuffer = this.stdoutBuffer.subarray(newlineIndex + 1);
|
|
198
|
-
|
|
199
|
-
const line = lineBuffer.toString("utf8").trim();
|
|
200
|
-
if (!line) return true;
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
const message = JSON.parse(line);
|
|
204
|
-
this.handleMessage(message);
|
|
205
|
-
} catch {
|
|
206
|
-
this.logger.debug("Failed to parse stdout JSON:", line);
|
|
207
|
-
}
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private parseLspMessageFromBuffer(): boolean {
|
|
212
|
-
const separator = Buffer.from("\r\n\r\n");
|
|
213
|
-
let headerEndIndex = this.stdoutBuffer.indexOf(separator);
|
|
214
|
-
let headerLength = separator.length;
|
|
215
|
-
|
|
216
|
-
if (headerEndIndex === -1) {
|
|
217
|
-
const altSeparator = Buffer.from("\n\n");
|
|
218
|
-
headerEndIndex = this.stdoutBuffer.indexOf(altSeparator);
|
|
219
|
-
headerLength = altSeparator.length;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (headerEndIndex === -1) return false;
|
|
223
|
-
|
|
224
|
-
const headerText = this.stdoutBuffer.subarray(0, headerEndIndex).toString("utf8");
|
|
225
|
-
const contentLengthMatch = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
226
|
-
if (!contentLengthMatch) {
|
|
227
|
-
this.logger.warn("[mcp-bridge] Missing Content-Length in LSP-framed stdout message; dropping malformed frame");
|
|
228
|
-
this.stdoutBuffer = this.stdoutBuffer.subarray(headerEndIndex + headerLength);
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const contentLength = Number.parseInt(contentLengthMatch[1], 10);
|
|
233
|
-
const bodyStart = headerEndIndex + headerLength;
|
|
234
|
-
const bodyEnd = bodyStart + contentLength;
|
|
235
|
-
|
|
236
|
-
if (this.stdoutBuffer.length < bodyEnd) return false;
|
|
237
|
-
|
|
238
|
-
const body = this.stdoutBuffer.subarray(bodyStart, bodyEnd).toString("utf8");
|
|
239
|
-
this.stdoutBuffer = this.stdoutBuffer.subarray(bodyEnd);
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
const message = JSON.parse(body);
|
|
243
|
-
this.handleMessage(message);
|
|
244
|
-
} catch {
|
|
245
|
-
this.logger.debug("Failed to parse LSP stdout JSON:", body);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async disconnect(): Promise<void> {
|
|
252
|
-
this.isShuttingDown = true;
|
|
253
|
-
this.connected = false;
|
|
254
|
-
this.cleanupReconnectTimer();
|
|
255
|
-
|
|
256
|
-
const activeProcess = this.process;
|
|
257
|
-
if (activeProcess) {
|
|
258
|
-
if (activeProcess.stdin && !activeProcess.stdin.destroyed) {
|
|
259
|
-
try {
|
|
260
|
-
this.writeMessage({ jsonrpc: "2.0", method: "close" });
|
|
261
|
-
} catch {
|
|
262
|
-
this.logger.debug("[mcp-bridge] Failed to send close notification during stdio disconnect");
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
await this.terminateProcessGracefully(activeProcess);
|
|
267
|
-
if (this.process === activeProcess) {
|
|
268
|
-
this.process = null;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
this.rejectAllPending("Connection closed");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
isConnected(): boolean {
|
|
276
|
-
return this.connected && this.process !== null;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
private async terminateProcessGracefully(proc: ChildProcess): Promise<void> {
|
|
280
|
-
if (proc.exitCode !== null || proc.killed) return;
|
|
281
|
-
|
|
282
|
-
await new Promise<void>((resolve) => {
|
|
283
|
-
let done = false;
|
|
284
|
-
let forceKillTimer: NodeJS.Timeout | null = null;
|
|
285
|
-
|
|
286
|
-
const finish = () => {
|
|
287
|
-
if (done) return;
|
|
288
|
-
done = true;
|
|
289
|
-
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
290
|
-
proc.off("exit", onExit);
|
|
291
|
-
resolve();
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const onExit = () => finish();
|
|
295
|
-
proc.once("exit", onExit);
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
proc.kill("SIGINT");
|
|
299
|
-
} catch {
|
|
300
|
-
finish();
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
forceKillTimer = setTimeout(() => {
|
|
305
|
-
if (proc.exitCode === null && !proc.killed) {
|
|
306
|
-
try { proc.kill("SIGTERM"); } catch { /* ignore */ }
|
|
307
|
-
}
|
|
308
|
-
setTimeout(finish, 200);
|
|
309
|
-
}, 2000);
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { McpRequest, McpResponse, McpServerConfig, nextRequestId } from "./types.js";
|
|
2
|
-
import { BaseTransport, resolveEnvRecord, warnIfNonTlsRemoteUrl } from "./transport-base.js";
|
|
3
|
-
|
|
4
|
-
export class StreamableHttpTransport extends BaseTransport {
|
|
5
|
-
private sessionId?: string;
|
|
6
|
-
|
|
7
|
-
protected get transportName(): string { return "streamable-http"; }
|
|
8
|
-
|
|
9
|
-
async connect(): Promise<void> {
|
|
10
|
-
if (!this.config.url) {
|
|
11
|
-
throw new Error("Streamable HTTP transport requires URL");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
warnIfNonTlsRemoteUrl(this.config.url, this.logger);
|
|
15
|
-
// Validate that all header env vars resolve (fail fast)
|
|
16
|
-
resolveEnvRecord(this.config.headers || {}, "header");
|
|
17
|
-
await this.probeServer();
|
|
18
|
-
|
|
19
|
-
this.connected = true;
|
|
20
|
-
this.backoffDelay = this.clientConfig.reconnectIntervalMs || 30000;
|
|
21
|
-
this.logger.info(`[mcp-bridge] Streamable HTTP transport ready for ${this.config.url}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async sendRequest(request: McpRequest): Promise<McpResponse> {
|
|
25
|
-
if (!this.connected || !this.config.url) {
|
|
26
|
-
throw new Error("Streamable HTTP transport not connected");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const id = nextRequestId();
|
|
30
|
-
const requestWithId = { ...request, id };
|
|
31
|
-
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const requestTimeout = this.clientConfig.requestTimeoutMs || 60000;
|
|
34
|
-
const timeout = setTimeout(() => {
|
|
35
|
-
this.pendingRequests.delete(id);
|
|
36
|
-
reject(new Error(`Request timeout after ${requestTimeout}ms`));
|
|
37
|
-
}, requestTimeout);
|
|
38
|
-
|
|
39
|
-
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
40
|
-
|
|
41
|
-
const headers = resolveEnvRecord({
|
|
42
|
-
"Accept": "application/json, text/event-stream",
|
|
43
|
-
...this.config.headers,
|
|
44
|
-
"Content-Type": "application/json"
|
|
45
|
-
}, "header");
|
|
46
|
-
|
|
47
|
-
if (this.sessionId) {
|
|
48
|
-
headers["mcp-session-id"] = this.sessionId;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
fetch(this.config.url!, {
|
|
52
|
-
method: "POST",
|
|
53
|
-
headers,
|
|
54
|
-
body: JSON.stringify(requestWithId)
|
|
55
|
-
})
|
|
56
|
-
.then(async response => {
|
|
57
|
-
const responseSessionId = response.headers.get("mcp-session-id");
|
|
58
|
-
if (responseSessionId) {
|
|
59
|
-
this.sessionId = responseSessionId;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
clearTimeout(timeout);
|
|
64
|
-
this.pendingRequests.delete(id);
|
|
65
|
-
reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const contentType = response.headers.get("content-type") || "";
|
|
71
|
-
let jsonResponse: any;
|
|
72
|
-
|
|
73
|
-
if (contentType.includes("text/event-stream")) {
|
|
74
|
-
const text = await response.text();
|
|
75
|
-
const dataLines = text.split('\n')
|
|
76
|
-
.filter((line: string) => line.startsWith('data:'))
|
|
77
|
-
.map((line: string) => line.substring(5).trim());
|
|
78
|
-
if (dataLines.length > 0) {
|
|
79
|
-
jsonResponse = JSON.parse(dataLines[dataLines.length - 1]);
|
|
80
|
-
} else {
|
|
81
|
-
throw new Error("No data lines in SSE response");
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
jsonResponse = await response.json();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
this.handleMessage(jsonResponse);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
clearTimeout(timeout);
|
|
90
|
-
this.pendingRequests.delete(id);
|
|
91
|
-
reject(new Error("Failed to parse response: " + (error instanceof Error ? error.message : String(error))));
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
.catch(error => {
|
|
95
|
-
clearTimeout(timeout);
|
|
96
|
-
this.pendingRequests.delete(id);
|
|
97
|
-
|
|
98
|
-
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
99
|
-
this.logger.error("Connection error, scheduling reconnect:", error.message);
|
|
100
|
-
this.scheduleReconnect();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
reject(error);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async sendNotification(notification: any): Promise<void> {
|
|
109
|
-
if (!this.connected || !this.config.url) {
|
|
110
|
-
throw new Error("Streamable HTTP transport not connected");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const headers = resolveEnvRecord({
|
|
114
|
-
"Accept": "application/json, text/event-stream",
|
|
115
|
-
...this.config.headers,
|
|
116
|
-
"Content-Type": "application/json"
|
|
117
|
-
}, "header");
|
|
118
|
-
|
|
119
|
-
if (this.sessionId) {
|
|
120
|
-
headers["mcp-session-id"] = this.sessionId;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const response = await fetch(this.config.url, {
|
|
125
|
-
method: "POST",
|
|
126
|
-
headers,
|
|
127
|
-
body: JSON.stringify(notification)
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const responseSessionId = response.headers.get("mcp-session-id");
|
|
131
|
-
if (responseSessionId) {
|
|
132
|
-
this.sessionId = responseSessionId;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!response.ok && response.status >= 500) {
|
|
136
|
-
throw new Error(`Server error: HTTP ${response.status}`);
|
|
137
|
-
}
|
|
138
|
-
} catch (error) {
|
|
139
|
-
if (error instanceof Error && error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
140
|
-
this.logger.error("Connection error during notification, scheduling reconnect:", error.message);
|
|
141
|
-
this.scheduleReconnect();
|
|
142
|
-
}
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private async probeServer(): Promise<void> {
|
|
148
|
-
if (!this.config.url) return;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const optionsResponse = await fetch(this.config.url, { method: "OPTIONS" });
|
|
152
|
-
if (optionsResponse.ok) return;
|
|
153
|
-
|
|
154
|
-
const headers = resolveEnvRecord(this.config.headers || {}, "header");
|
|
155
|
-
const headResponse = await fetch(this.config.url, { method: "HEAD", headers });
|
|
156
|
-
if (!headResponse.ok) {
|
|
157
|
-
this.logger.warn(`[mcp-bridge] Streamable HTTP server probe: OPTIONS ${optionsResponse.status}, HEAD ${headResponse.status} (non-blocking, connection continues)`);
|
|
158
|
-
}
|
|
159
|
-
} catch (error: any) {
|
|
160
|
-
this.logger.warn(`[mcp-bridge] Streamable HTTP server probe failed (non-blocking): ${error?.message || error}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
async disconnect(): Promise<void> {
|
|
165
|
-
this.connected = false;
|
|
166
|
-
this.cleanupReconnectTimer();
|
|
167
|
-
|
|
168
|
-
// Send DELETE request if we have a session to clean up
|
|
169
|
-
if (this.sessionId && this.config.url) {
|
|
170
|
-
try {
|
|
171
|
-
const headers = resolveEnvRecord(this.config.headers || {}, "header");
|
|
172
|
-
headers["mcp-session-id"] = this.sessionId;
|
|
173
|
-
|
|
174
|
-
await fetch(this.config.url, {
|
|
175
|
-
method: "DELETE",
|
|
176
|
-
headers
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
this.sessionId = undefined;
|
|
180
|
-
this.logger.info("Streamable HTTP session cleaned up");
|
|
181
|
-
} catch (error) {
|
|
182
|
-
this.logger.warn("Failed to clean up session on disconnect:", error);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
this.rejectAllPending("Connection closed");
|
|
187
|
-
}
|
|
188
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
export interface Logger {
|
|
2
|
-
info: (...args: unknown[]) => void;
|
|
3
|
-
warn: (...args: unknown[]) => void;
|
|
4
|
-
error: (...args: unknown[]) => void;
|
|
5
|
-
debug: (...args: unknown[]) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface McpServerConfig {
|
|
9
|
-
transport: "sse" | "stdio" | "streamable-http";
|
|
10
|
-
/** Human-readable description for router tool description generation */
|
|
11
|
-
description?: string;
|
|
12
|
-
// SSE transport
|
|
13
|
-
url?: string;
|
|
14
|
-
headers?: Record<string, string>;
|
|
15
|
-
// Stdio transport
|
|
16
|
-
command?: string;
|
|
17
|
-
args?: string[];
|
|
18
|
-
env?: Record<string, string>;
|
|
19
|
-
// Stdio framing override (default: auto-detect from first message)
|
|
20
|
-
framing?: "auto" | "lsp" | "newline";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface McpClientConfig {
|
|
24
|
-
servers: Record<string, McpServerConfig>;
|
|
25
|
-
mode?: "direct" | "router";
|
|
26
|
-
toolPrefix?: boolean | "auto";
|
|
27
|
-
reconnectIntervalMs?: number;
|
|
28
|
-
connectionTimeoutMs?: number;
|
|
29
|
-
requestTimeoutMs?: number;
|
|
30
|
-
routerIdleTimeoutMs?: number;
|
|
31
|
-
routerMaxConcurrent?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface McpTool {
|
|
35
|
-
name: string;
|
|
36
|
-
description: string;
|
|
37
|
-
inputSchema: any; // JSON Schema
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface McpRequest {
|
|
41
|
-
jsonrpc: "2.0";
|
|
42
|
-
id?: number;
|
|
43
|
-
method: string;
|
|
44
|
-
params?: any;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
let globalRequestId = 1;
|
|
48
|
-
|
|
49
|
-
export function nextRequestId(): number {
|
|
50
|
-
return globalRequestId++;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface McpResponse {
|
|
54
|
-
jsonrpc: "2.0";
|
|
55
|
-
id: number;
|
|
56
|
-
result?: any;
|
|
57
|
-
error?: {
|
|
58
|
-
code: number;
|
|
59
|
-
message: string;
|
|
60
|
-
data?: any;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface McpTransport {
|
|
65
|
-
connect(): Promise<void>;
|
|
66
|
-
disconnect(): Promise<void>;
|
|
67
|
-
sendRequest(request: McpRequest): Promise<McpResponse>;
|
|
68
|
-
sendNotification(notification: any): Promise<void>;
|
|
69
|
-
isConnected(): boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface McpServerConnection {
|
|
73
|
-
name: string;
|
|
74
|
-
transport: McpTransport;
|
|
75
|
-
tools: McpTool[];
|
|
76
|
-
isInitialized: boolean;
|
|
77
|
-
registeredToolNames: string[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Bridge-level config loaded from ~/.mcp-bridge/config.json */
|
|
81
|
-
export interface BridgeConfig extends McpClientConfig {
|
|
82
|
-
http?: {
|
|
83
|
-
auth?: {
|
|
84
|
-
type: "bearer";
|
|
85
|
-
token: string;
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
}
|