@winstonsayno/mcp-gateway 0.0.0-stage → 0.4.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/CHANGELOG.md +130 -0
- package/LICENSE +21 -0
- package/README.md +432 -2
- package/dashboard/README.md +27 -0
- package/dashboard/index.html +415 -0
- package/dist/auth/middleware.d.ts +24 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +108 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/ratelimit.d.ts +19 -0
- package/dist/auth/ratelimit.d.ts.map +1 -0
- package/dist/auth/ratelimit.js +81 -0
- package/dist/auth/ratelimit.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +129 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +276 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/watcher.d.ts +29 -0
- package/dist/config/watcher.d.ts.map +1 -0
- package/dist/config/watcher.js +96 -0
- package/dist/config/watcher.js.map +1 -0
- package/dist/gateway/api.d.ts +47 -0
- package/dist/gateway/api.d.ts.map +1 -0
- package/dist/gateway/api.js +324 -0
- package/dist/gateway/api.js.map +1 -0
- package/dist/gateway/index.d.ts +49 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +261 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/supervisor.d.ts +58 -0
- package/dist/gateway/supervisor.d.ts.map +1 -0
- package/dist/gateway/supervisor.js +205 -0
- package/dist/gateway/supervisor.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/cors.d.ts +19 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +65 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +40 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +98 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/request-id.d.ts +23 -0
- package/dist/middleware/request-id.d.ts.map +1 -0
- package/dist/middleware/request-id.js +30 -0
- package/dist/middleware/request-id.js.map +1 -0
- package/dist/middleware/timeout.d.ts +12 -0
- package/dist/middleware/timeout.d.ts.map +1 -0
- package/dist/middleware/timeout.js +39 -0
- package/dist/middleware/timeout.js.map +1 -0
- package/dist/monitor/index.d.ts +49 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/monitor/index.js +204 -0
- package/dist/monitor/index.js.map +1 -0
- package/dist/proxy/index.d.ts +86 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +389 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/registry/index.d.ts +48 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +171 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/transport/channel.d.ts +61 -0
- package/dist/transport/channel.d.ts.map +1 -0
- package/dist/transport/channel.js +38 -0
- package/dist/transport/channel.js.map +1 -0
- package/dist/transport/sse-parser.d.ts +30 -0
- package/dist/transport/sse-parser.d.ts.map +1 -0
- package/dist/transport/sse-parser.js +107 -0
- package/dist/transport/sse-parser.js.map +1 -0
- package/dist/transport/sse.d.ts +39 -0
- package/dist/transport/sse.d.ts.map +1 -0
- package/dist/transport/sse.js +142 -0
- package/dist/transport/sse.js.map +1 -0
- package/dist/transport/stdio.d.ts +35 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +149 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport/streamable-http.d.ts +45 -0
- package/dist/transport/streamable-http.d.ts.map +1 -0
- package/dist/transport/streamable-http.js +183 -0
- package/dist/transport/streamable-http.js.map +1 -0
- package/dist/transport/websocket.d.ts +38 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +172 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +60 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mutex.d.ts +25 -0
- package/dist/utils/mutex.d.ts.map +1 -0
- package/dist/utils/mutex.js +62 -0
- package/dist/utils/mutex.js.map +1 -0
- package/dist/utils/semaphore.d.ts +16 -0
- package/dist/utils/semaphore.d.ts.map +1 -0
- package/dist/utils/semaphore.js +42 -0
- package/dist/utils/semaphore.js.map +1 -0
- package/dist/utils/tool-filter.d.ts +24 -0
- package/dist/utils/tool-filter.d.ts.map +1 -0
- package/dist/utils/tool-filter.js +48 -0
- package/dist/utils/tool-filter.js.map +1 -0
- package/dist/utils/types.d.ts +223 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +5 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/utils/version.d.ts +6 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +18 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +79 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio channel — spawns the MCP server as a child process and exchanges
|
|
3
|
+
* newline-delimited JSON-RPC over its stdin/stdout (MCP stdio transport).
|
|
4
|
+
*
|
|
5
|
+
* Carries over the v0.2 / audit fixes:
|
|
6
|
+
* - stdout is decoded as a stream so multi-byte UTF-8 split across chunks survives;
|
|
7
|
+
* - stream errors (EPIPE on a dead child) never become unhandled 'error' events;
|
|
8
|
+
* - spawn errors and unexpected exits are reported once through `onclose`;
|
|
9
|
+
* - shutdown follows the MCP spec: close stdin → SIGTERM → SIGKILL, waiting
|
|
10
|
+
* for the real exit instead of trusting `proc.killed`;
|
|
11
|
+
* - un-terminated stdout is capped.
|
|
12
|
+
*
|
|
13
|
+
* @module transport/stdio
|
|
14
|
+
*/
|
|
15
|
+
import type { McpServerConfig } from '../utils/types.js';
|
|
16
|
+
import { type ChannelOptions, type JsonRpcMessage, type UpstreamChannel } from './channel.js';
|
|
17
|
+
export declare class StdioChannel implements UpstreamChannel {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly options;
|
|
20
|
+
readonly kind = "stdio";
|
|
21
|
+
onmessage?: (message: JsonRpcMessage) => void;
|
|
22
|
+
onclose?: (error: Error) => void;
|
|
23
|
+
private proc?;
|
|
24
|
+
private buffer;
|
|
25
|
+
private closing;
|
|
26
|
+
private lost;
|
|
27
|
+
private exited;
|
|
28
|
+
constructor(config: McpServerConfig, options: ChannelOptions);
|
|
29
|
+
start(): Promise<void>;
|
|
30
|
+
private lose;
|
|
31
|
+
private drain;
|
|
32
|
+
send(message: JsonRpcMessage): Promise<void>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=stdio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/transport/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAA2B,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAIvH,qBAAa,YAAa,YAAW,eAAe;IAYhD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZ1B,QAAQ,CAAC,IAAI,WAAW;IACxB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC,OAAO,CAAC,IAAI,CAAC,CAAe;IAC5B,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,MAAM,CAAiB;gBAGZ,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,cAAc;IAG1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2DtB,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,KAAK;IAiBP,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA2B7B"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio channel — spawns the MCP server as a child process and exchanges
|
|
3
|
+
* newline-delimited JSON-RPC over its stdin/stdout (MCP stdio transport).
|
|
4
|
+
*
|
|
5
|
+
* Carries over the v0.2 / audit fixes:
|
|
6
|
+
* - stdout is decoded as a stream so multi-byte UTF-8 split across chunks survives;
|
|
7
|
+
* - stream errors (EPIPE on a dead child) never become unhandled 'error' events;
|
|
8
|
+
* - spawn errors and unexpected exits are reported once through `onclose`;
|
|
9
|
+
* - shutdown follows the MCP spec: close stdin → SIGTERM → SIGKILL, waiting
|
|
10
|
+
* for the real exit instead of trusting `proc.killed`;
|
|
11
|
+
* - un-terminated stdout is capped.
|
|
12
|
+
*
|
|
13
|
+
* @module transport/stdio
|
|
14
|
+
*/
|
|
15
|
+
import { spawn } from 'child_process';
|
|
16
|
+
import { logger } from '../utils/logger.js';
|
|
17
|
+
import { expandEnv, expandRecord } from './channel.js';
|
|
18
|
+
const MAX_BUFFER_CHARS = 16 * 1024 * 1024;
|
|
19
|
+
export class StdioChannel {
|
|
20
|
+
config;
|
|
21
|
+
options;
|
|
22
|
+
kind = 'stdio';
|
|
23
|
+
onmessage;
|
|
24
|
+
onclose;
|
|
25
|
+
proc;
|
|
26
|
+
buffer = '';
|
|
27
|
+
closing = false;
|
|
28
|
+
lost = false;
|
|
29
|
+
exited;
|
|
30
|
+
constructor(config, options) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
start() {
|
|
35
|
+
const { config } = this;
|
|
36
|
+
if (!config.command)
|
|
37
|
+
return Promise.reject(new Error(`Server "${config.id}" has no command configured`));
|
|
38
|
+
// ${VAR} is expanded in args too (the examples pass e.g. ${DATABASE_URL} as an argument).
|
|
39
|
+
const proc = spawn(config.command, (config.args ?? []).map(expandEnv), {
|
|
40
|
+
env: { ...process.env, ...expandRecord(config.env) },
|
|
41
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
42
|
+
});
|
|
43
|
+
this.proc = proc;
|
|
44
|
+
let markExited;
|
|
45
|
+
this.exited = new Promise((r) => (markExited = r));
|
|
46
|
+
proc.stdout?.setEncoding('utf8');
|
|
47
|
+
proc.stdout?.on('data', (chunk) => {
|
|
48
|
+
this.buffer += chunk;
|
|
49
|
+
this.drain();
|
|
50
|
+
if (this.buffer.length > MAX_BUFFER_CHARS) {
|
|
51
|
+
logger.error(`[${config.id}] stdout line exceeded ${MAX_BUFFER_CHARS} chars; dropping session`);
|
|
52
|
+
this.buffer = '';
|
|
53
|
+
this.lose(new Error('MCP server sent an oversized message'));
|
|
54
|
+
proc.kill('SIGKILL');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
proc.stderr?.on('data', (chunk) => {
|
|
58
|
+
logger.debug(`[${config.id}] stderr: ${chunk.toString().trim()}`);
|
|
59
|
+
});
|
|
60
|
+
const onStreamError = (err) => logger.debug(`[${config.id}] stdio stream error: ${err.message}`);
|
|
61
|
+
proc.stdin?.on('error', onStreamError);
|
|
62
|
+
proc.stdout?.on('error', onStreamError);
|
|
63
|
+
proc.stderr?.on('error', onStreamError);
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
let started = false;
|
|
66
|
+
proc.once('spawn', () => {
|
|
67
|
+
started = true;
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
proc.on('error', (err) => {
|
|
71
|
+
logger.error(`[${config.id}] spawn error: ${err.message}`);
|
|
72
|
+
markExited(); // 'exit' may never fire if spawn failed
|
|
73
|
+
if (!started) {
|
|
74
|
+
started = true;
|
|
75
|
+
this.lost = true;
|
|
76
|
+
reject(err);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.lose(err);
|
|
80
|
+
});
|
|
81
|
+
proc.on('exit', (code, signal) => {
|
|
82
|
+
markExited();
|
|
83
|
+
if (!this.closing)
|
|
84
|
+
logger.warn(`Server "${config.id}" exited (code=${code}, signal=${signal})`);
|
|
85
|
+
this.lose(new Error(`MCP server "${config.id}" exited unexpectedly (code=${code}, signal=${signal})`));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
lose(err) {
|
|
90
|
+
if (this.lost || this.closing)
|
|
91
|
+
return;
|
|
92
|
+
this.lost = true;
|
|
93
|
+
this.onclose?.(err);
|
|
94
|
+
}
|
|
95
|
+
drain() {
|
|
96
|
+
if (!this.buffer.includes('\n'))
|
|
97
|
+
return;
|
|
98
|
+
const lines = this.buffer.split('\n');
|
|
99
|
+
this.buffer = lines.pop() ?? '';
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed)
|
|
103
|
+
continue;
|
|
104
|
+
let msg;
|
|
105
|
+
try {
|
|
106
|
+
msg = JSON.parse(trimmed);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue; // Non-JSON line (some servers log to stdout) — ignore
|
|
110
|
+
}
|
|
111
|
+
if (msg && typeof msg === 'object')
|
|
112
|
+
this.onmessage?.(msg);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async send(message) {
|
|
116
|
+
const stdin = this.proc?.stdin;
|
|
117
|
+
if (this.lost || this.closing || !stdin || !stdin.writable) {
|
|
118
|
+
throw new Error(`Server "${this.config.id}" is not writable`);
|
|
119
|
+
}
|
|
120
|
+
stdin.write(JSON.stringify(message) + '\n');
|
|
121
|
+
}
|
|
122
|
+
async close() {
|
|
123
|
+
if (this.closing)
|
|
124
|
+
return;
|
|
125
|
+
this.closing = true;
|
|
126
|
+
const proc = this.proc;
|
|
127
|
+
if (!proc)
|
|
128
|
+
return;
|
|
129
|
+
const grace = this.options.killGraceMs;
|
|
130
|
+
const isRunning = () => proc.exitCode === null && proc.signalCode === null && proc.pid !== undefined;
|
|
131
|
+
const waitExit = (ms) => Promise.race([this.exited, new Promise((r) => setTimeout(r, ms).unref())]);
|
|
132
|
+
if (isRunning()) {
|
|
133
|
+
proc.stdin?.end();
|
|
134
|
+
await waitExit(Math.min(grace, 500));
|
|
135
|
+
}
|
|
136
|
+
if (isRunning()) {
|
|
137
|
+
proc.kill('SIGTERM');
|
|
138
|
+
await waitExit(grace);
|
|
139
|
+
}
|
|
140
|
+
if (isRunning()) {
|
|
141
|
+
proc.kill('SIGKILL');
|
|
142
|
+
await waitExit(grace);
|
|
143
|
+
}
|
|
144
|
+
proc.stdin?.destroy();
|
|
145
|
+
proc.stdout?.destroy();
|
|
146
|
+
proc.stderr?.destroy();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/transport/stdio.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP channel — the current MCP HTTP transport (protocol 2025-03-26+).
|
|
3
|
+
*
|
|
4
|
+
* - Every client→server message is a `POST <url>` with
|
|
5
|
+
* `Accept: application/json, text/event-stream`.
|
|
6
|
+
* - The server answers `202 Accepted` (notifications / responses), a single
|
|
7
|
+
* `application/json` body, or a `text/event-stream` that carries the
|
|
8
|
+
* response (plus any server requests/notifications sent before it).
|
|
9
|
+
* - A session id returned in `Mcp-Session-Id` on `initialize` is echoed on
|
|
10
|
+
* every later request; the negotiated version goes in `MCP-Protocol-Version`.
|
|
11
|
+
* - `close()` ends the session with `DELETE` (best effort).
|
|
12
|
+
*
|
|
13
|
+
* Not implemented: the optional standalone `GET` stream for unsolicited server
|
|
14
|
+
* notifications, and SSE stream resumption. Neither is needed for tool calls.
|
|
15
|
+
*
|
|
16
|
+
* @module transport/streamable-http
|
|
17
|
+
*/
|
|
18
|
+
import type { McpServerConfig } from '../utils/types.js';
|
|
19
|
+
import { type ChannelOptions, type JsonRpcId, type JsonRpcMessage, type UpstreamChannel } from './channel.js';
|
|
20
|
+
export declare class StreamableHttpChannel implements UpstreamChannel {
|
|
21
|
+
private readonly config;
|
|
22
|
+
private readonly options;
|
|
23
|
+
readonly kind = "streamable-http";
|
|
24
|
+
onmessage?: (message: JsonRpcMessage) => void;
|
|
25
|
+
onclose?: (error: Error) => void;
|
|
26
|
+
private readonly url;
|
|
27
|
+
private readonly headers;
|
|
28
|
+
private sessionId?;
|
|
29
|
+
private protocolVersion?;
|
|
30
|
+
private closed;
|
|
31
|
+
private lost;
|
|
32
|
+
/** Open POST requests, keyed by JSON-RPC request id when they carry one. */
|
|
33
|
+
private readonly inflight;
|
|
34
|
+
constructor(config: McpServerConfig, options: ChannelOptions);
|
|
35
|
+
start(): Promise<void>;
|
|
36
|
+
setProtocolVersion(version: string): void;
|
|
37
|
+
private requestHeaders;
|
|
38
|
+
send(message: JsonRpcMessage): Promise<void>;
|
|
39
|
+
private deliver;
|
|
40
|
+
abandon(id: JsonRpcId): void;
|
|
41
|
+
private lose;
|
|
42
|
+
private abortAll;
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=streamable-http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamable-http.d.ts","sourceRoot":"","sources":["../../src/transport/streamable-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAGtB,qBAAa,qBAAsB,YAAW,eAAe;IAezD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAf1B,QAAQ,CAAC,IAAI,qBAAqB;IAClC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;IACjD,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAS;IACrB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkD;gBAGxD,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,cAAc;IAOpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIzC,OAAO,CAAC,cAAc;IAOhB,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ElD,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI;IAK5B,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,QAAQ;IAKV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAkB7B"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP channel — the current MCP HTTP transport (protocol 2025-03-26+).
|
|
3
|
+
*
|
|
4
|
+
* - Every client→server message is a `POST <url>` with
|
|
5
|
+
* `Accept: application/json, text/event-stream`.
|
|
6
|
+
* - The server answers `202 Accepted` (notifications / responses), a single
|
|
7
|
+
* `application/json` body, or a `text/event-stream` that carries the
|
|
8
|
+
* response (plus any server requests/notifications sent before it).
|
|
9
|
+
* - A session id returned in `Mcp-Session-Id` on `initialize` is echoed on
|
|
10
|
+
* every later request; the negotiated version goes in `MCP-Protocol-Version`.
|
|
11
|
+
* - `close()` ends the session with `DELETE` (best effort).
|
|
12
|
+
*
|
|
13
|
+
* Not implemented: the optional standalone `GET` stream for unsolicited server
|
|
14
|
+
* notifications, and SSE stream resumption. Neither is needed for tool calls.
|
|
15
|
+
*
|
|
16
|
+
* @module transport/streamable-http
|
|
17
|
+
*/
|
|
18
|
+
import { logger } from '../utils/logger.js';
|
|
19
|
+
import { errMessage, expandRecord, toMessages, } from './channel.js';
|
|
20
|
+
import { readSseStream } from './sse-parser.js';
|
|
21
|
+
export class StreamableHttpChannel {
|
|
22
|
+
config;
|
|
23
|
+
options;
|
|
24
|
+
kind = 'streamable-http';
|
|
25
|
+
onmessage;
|
|
26
|
+
onclose;
|
|
27
|
+
url;
|
|
28
|
+
headers;
|
|
29
|
+
sessionId;
|
|
30
|
+
protocolVersion;
|
|
31
|
+
closed = false;
|
|
32
|
+
lost = false;
|
|
33
|
+
/** Open POST requests, keyed by JSON-RPC request id when they carry one. */
|
|
34
|
+
inflight = new Map();
|
|
35
|
+
constructor(config, options) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.options = options;
|
|
38
|
+
if (!config.url)
|
|
39
|
+
throw new Error(`Server "${config.id}" has no url configured`);
|
|
40
|
+
this.url = new URL(config.url);
|
|
41
|
+
this.headers = expandRecord(config.headers);
|
|
42
|
+
}
|
|
43
|
+
async start() {
|
|
44
|
+
// Nothing to open: the session starts with the `initialize` POST.
|
|
45
|
+
}
|
|
46
|
+
setProtocolVersion(version) {
|
|
47
|
+
this.protocolVersion = version;
|
|
48
|
+
}
|
|
49
|
+
requestHeaders(extra = {}) {
|
|
50
|
+
const h = { ...this.headers, ...extra };
|
|
51
|
+
if (this.sessionId)
|
|
52
|
+
h['mcp-session-id'] = this.sessionId;
|
|
53
|
+
if (this.protocolVersion)
|
|
54
|
+
h['mcp-protocol-version'] = this.protocolVersion;
|
|
55
|
+
return h;
|
|
56
|
+
}
|
|
57
|
+
async send(message) {
|
|
58
|
+
if (this.closed || this.lost)
|
|
59
|
+
throw new Error('Streamable HTTP channel is closed');
|
|
60
|
+
const key = message.method !== undefined && message.id !== undefined && message.id !== null ? message.id : Symbol('post');
|
|
61
|
+
const abort = new AbortController();
|
|
62
|
+
this.inflight.set(key, abort);
|
|
63
|
+
const done = () => {
|
|
64
|
+
if (this.inflight.get(key) === abort)
|
|
65
|
+
this.inflight.delete(key);
|
|
66
|
+
};
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(this.url, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: this.requestHeaders({
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
Accept: 'application/json, text/event-stream',
|
|
74
|
+
}),
|
|
75
|
+
body: JSON.stringify(message),
|
|
76
|
+
redirect: 'error',
|
|
77
|
+
signal: abort.signal,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
done();
|
|
82
|
+
if (abort.signal.aborted)
|
|
83
|
+
throw new Error('Request aborted');
|
|
84
|
+
// The server is unreachable: whatever session it held is gone.
|
|
85
|
+
const e = new Error(`POST ${this.url.origin}${this.url.pathname} failed: ${errMessage(err)}`);
|
|
86
|
+
this.lose(e);
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
const sid = res.headers.get('mcp-session-id');
|
|
90
|
+
if (sid)
|
|
91
|
+
this.sessionId = sid;
|
|
92
|
+
if (res.status === 404 && this.sessionId && message.method !== 'initialize') {
|
|
93
|
+
done();
|
|
94
|
+
await res.body?.cancel().catch(() => { });
|
|
95
|
+
const e = new Error('MCP session expired (HTTP 404)');
|
|
96
|
+
this.lose(e);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
done();
|
|
101
|
+
const text = (await res.text().catch(() => '')).slice(0, 200);
|
|
102
|
+
throw new Error(`MCP server returned HTTP ${res.status}${text ? `: ${text}` : ''}`);
|
|
103
|
+
}
|
|
104
|
+
if (res.status === 202 || !res.body) {
|
|
105
|
+
done();
|
|
106
|
+
await res.body?.cancel().catch(() => { });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
110
|
+
if (contentType.includes('text/event-stream')) {
|
|
111
|
+
// Deliver events as they arrive; keep the stream open in the background.
|
|
112
|
+
void readSseStream(res.body, (ev) => {
|
|
113
|
+
if (ev.event !== 'message' || !ev.data)
|
|
114
|
+
return;
|
|
115
|
+
this.deliver(ev.data);
|
|
116
|
+
})
|
|
117
|
+
.catch((err) => {
|
|
118
|
+
if (!abort.signal.aborted)
|
|
119
|
+
logger.debug(`[${this.config.id}] response stream error: ${errMessage(err)}`);
|
|
120
|
+
})
|
|
121
|
+
.finally(done);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const text = await res.text();
|
|
126
|
+
if (text.trim())
|
|
127
|
+
this.deliver(text);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
done();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
deliver(text) {
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
logger.debug(`[${this.config.id}] ignoring non-JSON message`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const m of toMessages(parsed))
|
|
143
|
+
this.onmessage?.(m);
|
|
144
|
+
}
|
|
145
|
+
abandon(id) {
|
|
146
|
+
this.inflight.get(id)?.abort();
|
|
147
|
+
this.inflight.delete(id);
|
|
148
|
+
}
|
|
149
|
+
lose(err) {
|
|
150
|
+
if (this.closed || this.lost)
|
|
151
|
+
return;
|
|
152
|
+
this.lost = true;
|
|
153
|
+
this.abortAll();
|
|
154
|
+
this.onclose?.(err);
|
|
155
|
+
}
|
|
156
|
+
abortAll() {
|
|
157
|
+
for (const c of this.inflight.values())
|
|
158
|
+
c.abort();
|
|
159
|
+
this.inflight.clear();
|
|
160
|
+
}
|
|
161
|
+
async close() {
|
|
162
|
+
if (this.closed)
|
|
163
|
+
return;
|
|
164
|
+
this.closed = true;
|
|
165
|
+
this.abortAll();
|
|
166
|
+
if (!this.sessionId || this.lost)
|
|
167
|
+
return;
|
|
168
|
+
// Politely end the session; servers may answer 405 if they do not support it.
|
|
169
|
+
try {
|
|
170
|
+
const res = await fetch(this.url, {
|
|
171
|
+
method: 'DELETE',
|
|
172
|
+
headers: this.requestHeaders(),
|
|
173
|
+
redirect: 'error',
|
|
174
|
+
signal: AbortSignal.timeout(this.options.killGraceMs),
|
|
175
|
+
});
|
|
176
|
+
await res.body?.cancel().catch(() => { });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// best effort
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=streamable-http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streamable-http.js","sourceRoot":"","sources":["../../src/transport/streamable-http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket channel — one JSON-RPC message per text frame, using the `mcp`
|
|
3
|
+
* subprotocol by default (the same convention as the official SDK's
|
|
4
|
+
* `WebSocketClientTransport`; override per server with `subprotocol`). Uses the `ws` package so custom headers work
|
|
5
|
+
* and Node 20 (no global WebSocket) is supported.
|
|
6
|
+
*
|
|
7
|
+
* Keep-alive pings detect half-open connections: if a pong does not arrive
|
|
8
|
+
* before the next ping, the socket is terminated and reported as lost.
|
|
9
|
+
* Reconnection is handled by the gateway supervisor, which re-runs the MCP
|
|
10
|
+
* handshake on a fresh socket.
|
|
11
|
+
*
|
|
12
|
+
* @module transport/websocket
|
|
13
|
+
*/
|
|
14
|
+
import type { McpServerConfig } from '../utils/types.js';
|
|
15
|
+
import { type ChannelOptions, type JsonRpcMessage, type UpstreamChannel } from './channel.js';
|
|
16
|
+
export interface WebSocketChannelOptions extends ChannelOptions {
|
|
17
|
+
pingIntervalMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class WebSocketChannel implements UpstreamChannel {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly options;
|
|
22
|
+
readonly kind = "websocket";
|
|
23
|
+
onmessage?: (message: JsonRpcMessage) => void;
|
|
24
|
+
onclose?: (error: Error) => void;
|
|
25
|
+
private ws?;
|
|
26
|
+
private closed;
|
|
27
|
+
private lost;
|
|
28
|
+
private pingTimer?;
|
|
29
|
+
private awaitingPong;
|
|
30
|
+
constructor(config: McpServerConfig, options: WebSocketChannelOptions);
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
private lose;
|
|
33
|
+
private startPing;
|
|
34
|
+
private stopPing;
|
|
35
|
+
send(message: JsonRpcMessage): Promise<void>;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=websocket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAgB,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAK5G,MAAM,WAAW,uBAAwB,SAAQ,cAAc;IAC7D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,gBAAiB,YAAW,eAAe;IAYpD,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAZ1B,QAAQ,CAAC,IAAI,eAAe;IAC5B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC,OAAO,CAAC,EAAE,CAAC,CAAY;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,SAAS,CAAC,CAAiB;IACnC,OAAO,CAAC,YAAY,CAAS;gBAGV,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,uBAAuB;IAKnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmEtB,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,SAAS;IAqBjB,OAAO,CAAC,QAAQ;IAKhB,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAwB7B"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket channel — one JSON-RPC message per text frame, using the `mcp`
|
|
3
|
+
* subprotocol by default (the same convention as the official SDK's
|
|
4
|
+
* `WebSocketClientTransport`; override per server with `subprotocol`). Uses the `ws` package so custom headers work
|
|
5
|
+
* and Node 20 (no global WebSocket) is supported.
|
|
6
|
+
*
|
|
7
|
+
* Keep-alive pings detect half-open connections: if a pong does not arrive
|
|
8
|
+
* before the next ping, the socket is terminated and reported as lost.
|
|
9
|
+
* Reconnection is handled by the gateway supervisor, which re-runs the MCP
|
|
10
|
+
* handshake on a fresh socket.
|
|
11
|
+
*
|
|
12
|
+
* @module transport/websocket
|
|
13
|
+
*/
|
|
14
|
+
import WebSocket from 'ws';
|
|
15
|
+
import { logger } from '../utils/logger.js';
|
|
16
|
+
import { expandRecord } from './channel.js';
|
|
17
|
+
const SUBPROTOCOL = 'mcp';
|
|
18
|
+
const MAX_PAYLOAD = 16 * 1024 * 1024;
|
|
19
|
+
export class WebSocketChannel {
|
|
20
|
+
config;
|
|
21
|
+
options;
|
|
22
|
+
kind = 'websocket';
|
|
23
|
+
onmessage;
|
|
24
|
+
onclose;
|
|
25
|
+
ws;
|
|
26
|
+
closed = false;
|
|
27
|
+
lost = false;
|
|
28
|
+
pingTimer;
|
|
29
|
+
awaitingPong = false;
|
|
30
|
+
constructor(config, options) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.options = options;
|
|
33
|
+
if (!config.url)
|
|
34
|
+
throw new Error(`Server "${config.id}" has no url configured`);
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let settled = false;
|
|
39
|
+
// `subprotocol: ""` connects without requesting one (for servers that reject it).
|
|
40
|
+
const sub = this.config.subprotocol ?? SUBPROTOCOL;
|
|
41
|
+
const ws = new WebSocket(this.config.url, sub ? [sub] : [], {
|
|
42
|
+
headers: expandRecord(this.config.headers),
|
|
43
|
+
handshakeTimeout: this.options.connectTimeoutMs,
|
|
44
|
+
maxPayload: MAX_PAYLOAD,
|
|
45
|
+
});
|
|
46
|
+
this.ws = ws;
|
|
47
|
+
ws.on('open', () => {
|
|
48
|
+
settled = true;
|
|
49
|
+
this.startPing();
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
ws.on('unexpected-response', (_req, res) => {
|
|
53
|
+
res.resume();
|
|
54
|
+
const err = new Error(`WebSocket upgrade rejected with HTTP ${res.statusCode}`);
|
|
55
|
+
ws.terminate();
|
|
56
|
+
if (!settled) {
|
|
57
|
+
settled = true;
|
|
58
|
+
reject(err);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
ws.on('message', (raw, isBinary) => {
|
|
62
|
+
if (isBinary)
|
|
63
|
+
return;
|
|
64
|
+
let msg;
|
|
65
|
+
try {
|
|
66
|
+
msg = JSON.parse(raw.toString());
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
logger.debug(`[${this.config.id}] ignoring non-JSON WebSocket frame`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Batches are arrays of messages.
|
|
73
|
+
for (const m of Array.isArray(msg) ? msg : [msg]) {
|
|
74
|
+
if (m && typeof m === 'object')
|
|
75
|
+
this.onmessage?.(m);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
ws.on('pong', () => {
|
|
79
|
+
this.awaitingPong = false;
|
|
80
|
+
});
|
|
81
|
+
ws.on('error', (err) => {
|
|
82
|
+
logger.debug(`[${this.config.id}] WebSocket error: ${err.message}`);
|
|
83
|
+
if (!settled) {
|
|
84
|
+
settled = true;
|
|
85
|
+
reject(err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
ws.on('close', (code, reason) => {
|
|
89
|
+
this.stopPing();
|
|
90
|
+
if (!settled) {
|
|
91
|
+
settled = true;
|
|
92
|
+
reject(new Error(`WebSocket closed during connect (code=${code})`));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.lose(new Error(`WebSocket closed (code=${code}, reason=${reason.toString() || 'none'})`));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
lose(err) {
|
|
100
|
+
if (this.closed || this.lost)
|
|
101
|
+
return;
|
|
102
|
+
this.lost = true;
|
|
103
|
+
logger.warn(`[${this.config.id}] ${err.message}`);
|
|
104
|
+
this.onclose?.(err);
|
|
105
|
+
}
|
|
106
|
+
startPing() {
|
|
107
|
+
const interval = this.options.pingIntervalMs ?? 30_000;
|
|
108
|
+
if (!(interval > 0))
|
|
109
|
+
return;
|
|
110
|
+
this.pingTimer = setInterval(() => {
|
|
111
|
+
const ws = this.ws;
|
|
112
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
113
|
+
return;
|
|
114
|
+
if (this.awaitingPong) {
|
|
115
|
+
// No pong since the last ping: the connection is dead.
|
|
116
|
+
ws.terminate();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.awaitingPong = true;
|
|
120
|
+
try {
|
|
121
|
+
ws.ping();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// ignore — 'close' will follow
|
|
125
|
+
}
|
|
126
|
+
}, interval);
|
|
127
|
+
this.pingTimer.unref();
|
|
128
|
+
}
|
|
129
|
+
stopPing() {
|
|
130
|
+
if (this.pingTimer)
|
|
131
|
+
clearInterval(this.pingTimer);
|
|
132
|
+
this.pingTimer = undefined;
|
|
133
|
+
}
|
|
134
|
+
send(message) {
|
|
135
|
+
const ws = this.ws;
|
|
136
|
+
if (this.closed || this.lost || !ws || ws.readyState !== WebSocket.OPEN) {
|
|
137
|
+
return Promise.reject(new Error('WebSocket channel is not connected'));
|
|
138
|
+
}
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
ws.send(JSON.stringify(message), (err) => (err ? reject(err) : resolve()));
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async close() {
|
|
144
|
+
if (this.closed)
|
|
145
|
+
return;
|
|
146
|
+
this.closed = true;
|
|
147
|
+
this.stopPing();
|
|
148
|
+
const ws = this.ws;
|
|
149
|
+
if (!ws)
|
|
150
|
+
return;
|
|
151
|
+
if (ws.readyState === WebSocket.CLOSED)
|
|
152
|
+
return;
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
const force = setTimeout(() => {
|
|
155
|
+
ws.terminate();
|
|
156
|
+
resolve();
|
|
157
|
+
}, this.options.killGraceMs);
|
|
158
|
+
force.unref();
|
|
159
|
+
ws.once('close', () => {
|
|
160
|
+
clearTimeout(force);
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
try {
|
|
164
|
+
ws.close(1000, 'Client disconnect');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
ws.terminate();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=websocket.js.map
|