@winstonsayno/mcp-gateway 0.0.0-stage → 0.3.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/LICENSE +21 -0
  3. package/README.md +377 -2
  4. package/dashboard/README.md +27 -0
  5. package/dashboard/index.html +415 -0
  6. package/dist/auth/middleware.d.ts +24 -0
  7. package/dist/auth/middleware.d.ts.map +1 -0
  8. package/dist/auth/middleware.js +108 -0
  9. package/dist/auth/middleware.js.map +1 -0
  10. package/dist/auth/ratelimit.d.ts +19 -0
  11. package/dist/auth/ratelimit.d.ts.map +1 -0
  12. package/dist/auth/ratelimit.js +81 -0
  13. package/dist/auth/ratelimit.js.map +1 -0
  14. package/dist/cli.d.ts +6 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +129 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config/loader.d.ts +13 -0
  19. package/dist/config/loader.d.ts.map +1 -0
  20. package/dist/config/loader.js +268 -0
  21. package/dist/config/loader.js.map +1 -0
  22. package/dist/config/watcher.d.ts +29 -0
  23. package/dist/config/watcher.d.ts.map +1 -0
  24. package/dist/config/watcher.js +96 -0
  25. package/dist/config/watcher.js.map +1 -0
  26. package/dist/gateway/api.d.ts +29 -0
  27. package/dist/gateway/api.d.ts.map +1 -0
  28. package/dist/gateway/api.js +280 -0
  29. package/dist/gateway/api.js.map +1 -0
  30. package/dist/gateway/index.d.ts +49 -0
  31. package/dist/gateway/index.d.ts.map +1 -0
  32. package/dist/gateway/index.js +260 -0
  33. package/dist/gateway/index.js.map +1 -0
  34. package/dist/gateway/supervisor.d.ts +58 -0
  35. package/dist/gateway/supervisor.d.ts.map +1 -0
  36. package/dist/gateway/supervisor.js +205 -0
  37. package/dist/gateway/supervisor.js.map +1 -0
  38. package/dist/index.d.ts +15 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +12 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/middleware/cors.d.ts +19 -0
  43. package/dist/middleware/cors.d.ts.map +1 -0
  44. package/dist/middleware/cors.js +65 -0
  45. package/dist/middleware/cors.js.map +1 -0
  46. package/dist/middleware/error-handler.d.ts +40 -0
  47. package/dist/middleware/error-handler.d.ts.map +1 -0
  48. package/dist/middleware/error-handler.js +98 -0
  49. package/dist/middleware/error-handler.js.map +1 -0
  50. package/dist/middleware/request-id.d.ts +23 -0
  51. package/dist/middleware/request-id.d.ts.map +1 -0
  52. package/dist/middleware/request-id.js +30 -0
  53. package/dist/middleware/request-id.js.map +1 -0
  54. package/dist/middleware/timeout.d.ts +12 -0
  55. package/dist/middleware/timeout.d.ts.map +1 -0
  56. package/dist/middleware/timeout.js +39 -0
  57. package/dist/middleware/timeout.js.map +1 -0
  58. package/dist/monitor/index.d.ts +49 -0
  59. package/dist/monitor/index.d.ts.map +1 -0
  60. package/dist/monitor/index.js +204 -0
  61. package/dist/monitor/index.js.map +1 -0
  62. package/dist/proxy/index.d.ts +86 -0
  63. package/dist/proxy/index.d.ts.map +1 -0
  64. package/dist/proxy/index.js +389 -0
  65. package/dist/proxy/index.js.map +1 -0
  66. package/dist/registry/index.d.ts +41 -0
  67. package/dist/registry/index.d.ts.map +1 -0
  68. package/dist/registry/index.js +157 -0
  69. package/dist/registry/index.js.map +1 -0
  70. package/dist/transport/channel.d.ts +61 -0
  71. package/dist/transport/channel.d.ts.map +1 -0
  72. package/dist/transport/channel.js +38 -0
  73. package/dist/transport/channel.js.map +1 -0
  74. package/dist/transport/sse-parser.d.ts +30 -0
  75. package/dist/transport/sse-parser.d.ts.map +1 -0
  76. package/dist/transport/sse-parser.js +107 -0
  77. package/dist/transport/sse-parser.js.map +1 -0
  78. package/dist/transport/sse.d.ts +39 -0
  79. package/dist/transport/sse.d.ts.map +1 -0
  80. package/dist/transport/sse.js +142 -0
  81. package/dist/transport/sse.js.map +1 -0
  82. package/dist/transport/stdio.d.ts +35 -0
  83. package/dist/transport/stdio.d.ts.map +1 -0
  84. package/dist/transport/stdio.js +149 -0
  85. package/dist/transport/stdio.js.map +1 -0
  86. package/dist/transport/streamable-http.d.ts +45 -0
  87. package/dist/transport/streamable-http.d.ts.map +1 -0
  88. package/dist/transport/streamable-http.js +183 -0
  89. package/dist/transport/streamable-http.js.map +1 -0
  90. package/dist/transport/websocket.d.ts +38 -0
  91. package/dist/transport/websocket.d.ts.map +1 -0
  92. package/dist/transport/websocket.js +172 -0
  93. package/dist/transport/websocket.js.map +1 -0
  94. package/dist/utils/logger.d.ts +16 -0
  95. package/dist/utils/logger.d.ts.map +1 -0
  96. package/dist/utils/logger.js +60 -0
  97. package/dist/utils/logger.js.map +1 -0
  98. package/dist/utils/mutex.d.ts +25 -0
  99. package/dist/utils/mutex.d.ts.map +1 -0
  100. package/dist/utils/mutex.js +62 -0
  101. package/dist/utils/mutex.js.map +1 -0
  102. package/dist/utils/semaphore.d.ts +16 -0
  103. package/dist/utils/semaphore.d.ts.map +1 -0
  104. package/dist/utils/semaphore.js +42 -0
  105. package/dist/utils/semaphore.js.map +1 -0
  106. package/dist/utils/types.d.ts +215 -0
  107. package/dist/utils/types.d.ts.map +1 -0
  108. package/dist/utils/types.js +5 -0
  109. package/dist/utils/types.js.map +1 -0
  110. package/dist/utils/version.d.ts +6 -0
  111. package/dist/utils/version.d.ts.map +1 -0
  112. package/dist/utils/version.js +18 -0
  113. package/dist/utils/version.js.map +1 -0
  114. package/package.json +78 -4
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Minimal structured logger for mcp-gateway
3
+ */
4
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
5
+ export type { LogLevel };
6
+ export declare class Logger {
7
+ private level;
8
+ setLevel(level: LogLevel): void;
9
+ private log;
10
+ debug(message: string, meta?: Record<string, unknown>): void;
11
+ info(message: string, meta?: Record<string, unknown>): void;
12
+ warn(message: string, meta?: Record<string, unknown>): void;
13
+ error(message: string, meta?: Record<string, unknown>): void;
14
+ }
15
+ export declare const logger: Logger;
16
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAkBpD,YAAY,EAAE,QAAQ,EAAE,CAAC;AAEzB,qBAAa,MAAM;IACjB,OAAO,CAAC,KAAK,CAAoB;IAEjC,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI/B,OAAO,CAAC,GAAG;IAkBX,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI5D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;CAG7D;AAUD,eAAO,MAAM,MAAM,QAAe,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Minimal structured logger for mcp-gateway
3
+ */
4
+ const LEVELS = {
5
+ debug: 0,
6
+ info: 1,
7
+ warn: 2,
8
+ error: 3,
9
+ };
10
+ const COLORS = {
11
+ debug: '\x1b[90m', // gray
12
+ info: '\x1b[36m', // cyan
13
+ warn: '\x1b[33m', // yellow
14
+ error: '\x1b[31m', // red
15
+ };
16
+ const RESET = '\x1b[0m';
17
+ export class Logger {
18
+ level = 'info';
19
+ setLevel(level) {
20
+ this.level = level;
21
+ }
22
+ log(level, message, meta) {
23
+ if (LEVELS[level] < LEVELS[this.level])
24
+ return;
25
+ const ts = new Date().toISOString();
26
+ const color = process.stdout.isTTY ? COLORS[level] : '';
27
+ const reset = process.stdout.isTTY ? RESET : '';
28
+ const prefix = `${color}[${level.toUpperCase().padEnd(5)}]${reset}`;
29
+ const metaStr = meta ? ` ${safeStringify(meta)}` : '';
30
+ const output = `${ts} ${prefix} ${message}${metaStr}`;
31
+ if (level === 'error' || level === 'warn') {
32
+ process.stderr.write(output + '\n');
33
+ }
34
+ else {
35
+ process.stdout.write(output + '\n');
36
+ }
37
+ }
38
+ debug(message, meta) {
39
+ this.log('debug', message, meta);
40
+ }
41
+ info(message, meta) {
42
+ this.log('info', message, meta);
43
+ }
44
+ warn(message, meta) {
45
+ this.log('warn', message, meta);
46
+ }
47
+ error(message, meta) {
48
+ this.log('error', message, meta);
49
+ }
50
+ }
51
+ function safeStringify(value) {
52
+ try {
53
+ return JSON.stringify(value, (_k, v) => (typeof v === 'bigint' ? v.toString() : v));
54
+ }
55
+ catch {
56
+ return '[unserializable meta]';
57
+ }
58
+ }
59
+ export const logger = new Logger();
60
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Async Mutex
3
+ *
4
+ * Bug fix (v0.1.0): the server registry could attempt to restart the same
5
+ * MCP server process multiple times concurrently when several requests
6
+ * arrived simultaneously during a restart window, causing duplicate processes.
7
+ * This lightweight mutex serialises critical sections without external deps.
8
+ *
9
+ * @module utils/mutex
10
+ */
11
+ export declare class Mutex {
12
+ private _queue;
13
+ private _locked;
14
+ /**
15
+ * Acquire the lock. The returned release function is idempotent: calling it
16
+ * more than once is a no-op (previously a double release could unlock the
17
+ * mutex while another holder was still inside the critical section).
18
+ */
19
+ acquire(): Promise<() => void>;
20
+ /** Hand the lock directly to the next waiter, or unlock if none. */
21
+ private _release;
22
+ get isLocked(): boolean;
23
+ runExclusive<T>(fn: () => Promise<T>): Promise<T>;
24
+ }
25
+ //# sourceMappingURL=mutex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutex.d.ts","sourceRoot":"","sources":["../../src/utils/mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,qBAAa,KAAK;IAChB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,OAAO,CAAS;IAExB;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;IAmB9B,oEAAoE;IACpE,OAAO,CAAC,QAAQ;IAShB,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAEK,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAQxD"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Async Mutex
3
+ *
4
+ * Bug fix (v0.1.0): the server registry could attempt to restart the same
5
+ * MCP server process multiple times concurrently when several requests
6
+ * arrived simultaneously during a restart window, causing duplicate processes.
7
+ * This lightweight mutex serialises critical sections without external deps.
8
+ *
9
+ * @module utils/mutex
10
+ */
11
+ export class Mutex {
12
+ _queue = [];
13
+ _locked = false;
14
+ /**
15
+ * Acquire the lock. The returned release function is idempotent: calling it
16
+ * more than once is a no-op (previously a double release could unlock the
17
+ * mutex while another holder was still inside the critical section).
18
+ */
19
+ acquire() {
20
+ return new Promise((resolve) => {
21
+ const grant = () => {
22
+ let released = false;
23
+ resolve(() => {
24
+ if (released)
25
+ return;
26
+ released = true;
27
+ this._release();
28
+ });
29
+ };
30
+ if (!this._locked) {
31
+ this._locked = true;
32
+ grant();
33
+ }
34
+ else {
35
+ this._queue.push(grant);
36
+ }
37
+ });
38
+ }
39
+ /** Hand the lock directly to the next waiter, or unlock if none. */
40
+ _release() {
41
+ const next = this._queue.shift();
42
+ if (next) {
43
+ next(); // lock stays held, ownership transfers
44
+ }
45
+ else {
46
+ this._locked = false;
47
+ }
48
+ }
49
+ get isLocked() {
50
+ return this._locked;
51
+ }
52
+ async runExclusive(fn) {
53
+ const release = await this.acquire();
54
+ try {
55
+ return await fn();
56
+ }
57
+ finally {
58
+ release();
59
+ }
60
+ }
61
+ }
62
+ //# sourceMappingURL=mutex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../src/utils/mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,OAAO,KAAK;IACR,MAAM,GAAsB,EAAE,CAAC;IAC/B,OAAO,GAAG,KAAK,CAAC;IAExB;;;;OAIG;IACH,OAAO;QACL,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,MAAM,KAAK,GAAG,GAAG,EAAE;gBACjB,IAAI,QAAQ,GAAG,KAAK,CAAC;gBACrB,OAAO,CAAC,GAAG,EAAE;oBACX,IAAI,QAAQ;wBAAE,OAAO;oBACrB,QAAQ,GAAG,IAAI,CAAC;oBAChB,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC;YACF,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;gBACpB,KAAK,EAAE,CAAC;YACV,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,oEAAoE;IAC5D,QAAQ;QACd,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACjC,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,CAAC,CAAC,uCAAuC;QACjD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;QACvB,CAAC;IACH,CAAC;IAED,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,KAAK,CAAC,YAAY,CAAI,EAAoB;QACxC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,EAAE,CAAC;QACpB,CAAC;gBAAS,CAAC;YACT,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}