@wrongstack/mcp 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -0
- package/dist/index.d.ts +170 -10
- package/dist/index.js +771 -21
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,436 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
|
|
3
|
+
// src/client.ts
|
|
4
|
+
|
|
5
|
+
// src/transport.ts
|
|
6
|
+
var SSEReader = class {
|
|
7
|
+
buffer = "";
|
|
8
|
+
listeners = [];
|
|
9
|
+
onMessage(cb) {
|
|
10
|
+
this.listeners.push(cb);
|
|
11
|
+
return () => {
|
|
12
|
+
const idx = this.listeners.indexOf(cb);
|
|
13
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
feed(chunk) {
|
|
17
|
+
this.buffer += chunk;
|
|
18
|
+
let idx = this.buffer.indexOf("\n");
|
|
19
|
+
while (idx !== -1) {
|
|
20
|
+
const line = this.buffer.slice(0, idx);
|
|
21
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
22
|
+
idx = this.buffer.indexOf("\n");
|
|
23
|
+
if (line.startsWith("event:")) ; else if (line.startsWith("data:")) {
|
|
24
|
+
const data = line.slice(5).trim();
|
|
25
|
+
if (data) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(data);
|
|
28
|
+
this.dispatch(parsed);
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
dispatch(msg) {
|
|
36
|
+
for (const cb of this.listeners) {
|
|
37
|
+
try {
|
|
38
|
+
cb(msg);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
reset() {
|
|
44
|
+
this.buffer = "";
|
|
45
|
+
this.listeners = [];
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function isJsonRpcResult(v) {
|
|
49
|
+
return typeof v === "object" && v !== null && "jsonrpc" in v;
|
|
50
|
+
}
|
|
51
|
+
var SSETransport = class {
|
|
52
|
+
state = "idle";
|
|
53
|
+
url;
|
|
54
|
+
headers;
|
|
55
|
+
timeout;
|
|
56
|
+
nextId = 1;
|
|
57
|
+
pending = /* @__PURE__ */ new Map();
|
|
58
|
+
tools = [];
|
|
59
|
+
abortController;
|
|
60
|
+
reader;
|
|
61
|
+
readerDone = false;
|
|
62
|
+
disconnectHandlers = [];
|
|
63
|
+
readLoopAbort;
|
|
64
|
+
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
65
|
+
constructor(opts) {
|
|
66
|
+
this.url = opts.url;
|
|
67
|
+
this.headers = { ...opts.headers };
|
|
68
|
+
this.timeout = opts.startupTimeoutMs ?? 1e4;
|
|
69
|
+
}
|
|
70
|
+
getState() {
|
|
71
|
+
return this.state;
|
|
72
|
+
}
|
|
73
|
+
listTools() {
|
|
74
|
+
return [...this.tools];
|
|
75
|
+
}
|
|
76
|
+
onDisconnect(cb) {
|
|
77
|
+
this.disconnectHandlers.push(cb);
|
|
78
|
+
return () => {
|
|
79
|
+
const idx = this.disconnectHandlers.indexOf(cb);
|
|
80
|
+
if (idx >= 0) this.disconnectHandlers.splice(idx, 1);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
onToolsChanged(cb) {
|
|
84
|
+
this.toolsChangedListeners.add(cb);
|
|
85
|
+
return () => {
|
|
86
|
+
this.toolsChangedListeners.delete(cb);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Refresh tool list when server sends notifications/tools/list_changed. */
|
|
90
|
+
async handleToolsListChanged() {
|
|
91
|
+
try {
|
|
92
|
+
const res = await this.httpPost("tools/list", {});
|
|
93
|
+
if (!res.error) {
|
|
94
|
+
this.tools = res.result?.tools ?? [];
|
|
95
|
+
for (const cb of this.toolsChangedListeners) {
|
|
96
|
+
try {
|
|
97
|
+
cb([...this.tools]);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async connect() {
|
|
106
|
+
this.state = "connecting";
|
|
107
|
+
this.abortController = new AbortController();
|
|
108
|
+
const signal = this.abortController.signal;
|
|
109
|
+
const startupTimer = setTimeout(() => this.abortController?.abort(), this.timeout);
|
|
110
|
+
try {
|
|
111
|
+
const sseUrl = this.buildSSEUrl();
|
|
112
|
+
const response = await fetch(sseUrl, {
|
|
113
|
+
headers: this.headers,
|
|
114
|
+
signal
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(`SSE connect HTTP ${response.status}: ${response.statusText}`);
|
|
118
|
+
}
|
|
119
|
+
if (!response.body) {
|
|
120
|
+
throw new Error("SSE response has no body");
|
|
121
|
+
}
|
|
122
|
+
const textDecoder = new TextDecoder();
|
|
123
|
+
const sseReader = new SSEReader();
|
|
124
|
+
this.readLoopAbort = new AbortController();
|
|
125
|
+
sseReader.onMessage((msg) => {
|
|
126
|
+
if (msg.id !== void 0) {
|
|
127
|
+
const resolve = this.pending.get(msg.id);
|
|
128
|
+
if (resolve) {
|
|
129
|
+
this.pending.delete(msg.id);
|
|
130
|
+
resolve({ jsonrpc: "2.0", id: msg.id, result: msg.params });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (msg.method && !msg.id) {
|
|
134
|
+
if (msg.method === "notifications/tools/list_changed") {
|
|
135
|
+
void this.handleToolsListChanged();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const reader = response.body.getReader();
|
|
140
|
+
this.reader = {
|
|
141
|
+
cancel: () => reader.cancel(),
|
|
142
|
+
releaseLock: () => reader.releaseLock()
|
|
143
|
+
};
|
|
144
|
+
this.readSSEBody(reader, textDecoder, sseReader);
|
|
145
|
+
const initRes = await this.httpPost("initialize", {
|
|
146
|
+
protocolVersion: "2024-11-05",
|
|
147
|
+
capabilities: { tools: {} },
|
|
148
|
+
clientInfo: { name: "wrongstack", version: "0.1.1" }
|
|
149
|
+
});
|
|
150
|
+
if (initRes.error) {
|
|
151
|
+
throw new Error(`initialize failed: ${initRes.error.message}`);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await this.httpPost("notifications/initialized", {});
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
const toolsRes = await this.httpPost("tools/list", {});
|
|
158
|
+
if (toolsRes.error) {
|
|
159
|
+
this.tools = [];
|
|
160
|
+
} else {
|
|
161
|
+
const result = toolsRes.result;
|
|
162
|
+
this.tools = result?.tools ?? [];
|
|
163
|
+
}
|
|
164
|
+
this.state = "connected";
|
|
165
|
+
clearTimeout(startupTimer);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
clearTimeout(startupTimer);
|
|
168
|
+
this.state = "failed";
|
|
169
|
+
this.abortController.abort();
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async readSSEBody(reader, decoder, sseReader) {
|
|
174
|
+
try {
|
|
175
|
+
while (!this.readerDone) {
|
|
176
|
+
const { done, value } = await reader.read();
|
|
177
|
+
if (done) break;
|
|
178
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
179
|
+
sseReader.feed(chunk);
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
if (this.state !== "disconnected" && this.state !== "failed") {
|
|
183
|
+
this.state = "disconnected";
|
|
184
|
+
for (const cb of this.disconnectHandlers) {
|
|
185
|
+
try {
|
|
186
|
+
cb();
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
buildSSEUrl() {
|
|
194
|
+
try {
|
|
195
|
+
const url = new URL(this.url);
|
|
196
|
+
url.searchParams.set("session", String(Date.now()));
|
|
197
|
+
return url.toString();
|
|
198
|
+
} catch {
|
|
199
|
+
return this.url;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async httpPost(method, params) {
|
|
203
|
+
const id = this.nextId++;
|
|
204
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
205
|
+
const res = await fetch(this.url, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: {
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
...this.headers
|
|
210
|
+
},
|
|
211
|
+
body,
|
|
212
|
+
signal: this.abortController?.signal
|
|
213
|
+
});
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
216
|
+
}
|
|
217
|
+
const data = await res.json();
|
|
218
|
+
if (!isJsonRpcResult(data)) {
|
|
219
|
+
throw new Error("Invalid JSON-RPC response");
|
|
220
|
+
}
|
|
221
|
+
return data;
|
|
222
|
+
}
|
|
223
|
+
async callTool(name, input) {
|
|
224
|
+
if (this.state !== "connected") {
|
|
225
|
+
throw new Error(`SSE transport not connected (state=${this.state})`);
|
|
226
|
+
}
|
|
227
|
+
const res = await this.httpPost("tools/call", { name, arguments: input });
|
|
228
|
+
if (res.error) {
|
|
229
|
+
return { content: res.error.message, isError: true };
|
|
230
|
+
}
|
|
231
|
+
const result = res.result;
|
|
232
|
+
return {
|
|
233
|
+
content: result?.content ?? "",
|
|
234
|
+
isError: Boolean(result?.isError)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async close() {
|
|
238
|
+
if (this.state === "disconnected") return;
|
|
239
|
+
this.readerDone = true;
|
|
240
|
+
this.readLoopAbort?.abort();
|
|
241
|
+
try {
|
|
242
|
+
this.reader?.cancel();
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
this.reader?.releaseLock();
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
this.abortController?.abort();
|
|
250
|
+
this.disconnectHandlers = [];
|
|
251
|
+
this.state = "disconnected";
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var StreamableHTTPTransport = class {
|
|
255
|
+
state = "idle";
|
|
256
|
+
url;
|
|
257
|
+
headers;
|
|
258
|
+
timeout;
|
|
259
|
+
nextId = 1;
|
|
260
|
+
tools = [];
|
|
261
|
+
abortController;
|
|
262
|
+
sessionId;
|
|
263
|
+
disconnectHandlers = [];
|
|
264
|
+
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
265
|
+
constructor(opts) {
|
|
266
|
+
this.url = opts.url;
|
|
267
|
+
this.headers = { ...opts.headers };
|
|
268
|
+
this.timeout = opts.startupTimeoutMs ?? 1e4;
|
|
269
|
+
}
|
|
270
|
+
getState() {
|
|
271
|
+
return this.state;
|
|
272
|
+
}
|
|
273
|
+
listTools() {
|
|
274
|
+
return [...this.tools];
|
|
275
|
+
}
|
|
276
|
+
onDisconnect(cb) {
|
|
277
|
+
this.disconnectHandlers.push(cb);
|
|
278
|
+
return () => {
|
|
279
|
+
const idx = this.disconnectHandlers.indexOf(cb);
|
|
280
|
+
if (idx >= 0) this.disconnectHandlers.splice(idx, 1);
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
onToolsChanged(cb) {
|
|
284
|
+
this.toolsChangedListeners.add(cb);
|
|
285
|
+
return () => {
|
|
286
|
+
this.toolsChangedListeners.delete(cb);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async handleToolsListChanged() {
|
|
290
|
+
try {
|
|
291
|
+
const res = await this.postRaw("tools/list", {});
|
|
292
|
+
if (!res.error) {
|
|
293
|
+
this.tools = res.result?.tools ?? [];
|
|
294
|
+
for (const cb of this.toolsChangedListeners) {
|
|
295
|
+
try {
|
|
296
|
+
cb([...this.tools]);
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async connect() {
|
|
305
|
+
this.state = "connecting";
|
|
306
|
+
this.abortController = new AbortController();
|
|
307
|
+
const signal = this.abortController.signal;
|
|
308
|
+
const startupTimer = setTimeout(() => this.abortController?.abort(), this.timeout);
|
|
309
|
+
try {
|
|
310
|
+
const initRes = await fetch(this.url, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: {
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
"Accept": "application/json, text/event-stream",
|
|
315
|
+
...this.headers
|
|
316
|
+
},
|
|
317
|
+
body: JSON.stringify({
|
|
318
|
+
jsonrpc: "2.0",
|
|
319
|
+
id: this.nextId++,
|
|
320
|
+
method: "initialize",
|
|
321
|
+
params: {
|
|
322
|
+
protocolVersion: "2024-11-05",
|
|
323
|
+
capabilities: { tools: {} },
|
|
324
|
+
clientInfo: { name: "wrongstack", version: "0.1.1" }
|
|
325
|
+
}
|
|
326
|
+
}),
|
|
327
|
+
signal
|
|
328
|
+
});
|
|
329
|
+
if (!initRes.ok) {
|
|
330
|
+
throw new Error(`initialize HTTP ${initRes.status}: ${initRes.statusText}`);
|
|
331
|
+
}
|
|
332
|
+
const contentType = initRes.headers.get("content-type") ?? "";
|
|
333
|
+
let data;
|
|
334
|
+
if (contentType.includes("application/json")) {
|
|
335
|
+
const parsed = await initRes.json();
|
|
336
|
+
if (isJsonRpcResult(parsed)) data = parsed;
|
|
337
|
+
} else {
|
|
338
|
+
const text = await initRes.text();
|
|
339
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(line);
|
|
343
|
+
if (isJsonRpcResult(parsed)) {
|
|
344
|
+
data = parsed;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!data) {
|
|
353
|
+
throw new Error("Could not parse initialize response");
|
|
354
|
+
}
|
|
355
|
+
if (data.error) {
|
|
356
|
+
throw new Error(`initialize failed: ${data.error.message}`);
|
|
357
|
+
}
|
|
358
|
+
this.sessionId = initRes.headers.get("x-mcp-session") ?? void 0;
|
|
359
|
+
await this.postRaw("notifications/initialized", {});
|
|
360
|
+
const toolsRes = await this.postRaw("tools/list", {});
|
|
361
|
+
if (toolsRes.error) {
|
|
362
|
+
this.tools = [];
|
|
363
|
+
} else {
|
|
364
|
+
const result = toolsRes.result;
|
|
365
|
+
this.tools = result?.tools ?? [];
|
|
366
|
+
}
|
|
367
|
+
this.state = "connected";
|
|
368
|
+
clearTimeout(startupTimer);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
clearTimeout(startupTimer);
|
|
371
|
+
this.state = "failed";
|
|
372
|
+
this.abortController.abort();
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async postRaw(method, params) {
|
|
377
|
+
const id = this.nextId++;
|
|
378
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
379
|
+
const url = this.sessionId ? `${this.url}${this.url.includes("?") ? "&" : "?"}session=${this.sessionId}` : this.url;
|
|
380
|
+
const res = await fetch(url, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: {
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
"Accept": "application/json, text/event-stream",
|
|
385
|
+
...this.sessionId ? { "x-mcp-session": this.sessionId } : {},
|
|
386
|
+
...this.headers
|
|
387
|
+
},
|
|
388
|
+
body,
|
|
389
|
+
signal: this.abortController?.signal
|
|
390
|
+
});
|
|
391
|
+
if (!res.ok) {
|
|
392
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
393
|
+
}
|
|
394
|
+
const text = await res.text();
|
|
395
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
396
|
+
for (const line of lines) {
|
|
397
|
+
try {
|
|
398
|
+
const parsed = JSON.parse(line);
|
|
399
|
+
if (isJsonRpcResult(parsed)) return parsed;
|
|
400
|
+
} catch {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
throw new Error("Could not parse response as JSON-RPC");
|
|
405
|
+
}
|
|
406
|
+
async callTool(name, input) {
|
|
407
|
+
if (this.state !== "connected") {
|
|
408
|
+
throw new Error(`streamable-http transport not connected (state=${this.state})`);
|
|
409
|
+
}
|
|
410
|
+
const res = await this.postRaw("tools/call", { name, arguments: input });
|
|
411
|
+
if (res.error) {
|
|
412
|
+
return { content: res.error.message, isError: true };
|
|
413
|
+
}
|
|
414
|
+
const result = res.result;
|
|
415
|
+
return {
|
|
416
|
+
content: result?.content ?? "",
|
|
417
|
+
isError: Boolean(result?.isError)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async close() {
|
|
421
|
+
if (this.state === "disconnected") return;
|
|
422
|
+
this.state = "disconnected";
|
|
423
|
+
this.abortController?.abort();
|
|
424
|
+
for (const cb of this.disconnectHandlers) {
|
|
425
|
+
try {
|
|
426
|
+
cb();
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
this.disconnectHandlers = [];
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
3
434
|
// src/client.ts
|
|
4
435
|
var MCPClient = class {
|
|
5
436
|
constructor(opts) {
|
|
@@ -11,31 +442,64 @@ var MCPClient = class {
|
|
|
11
442
|
nextId = 1;
|
|
12
443
|
pending = /* @__PURE__ */ new Map();
|
|
13
444
|
rxBuffer = "";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
* returns false the first waiter sets this flag; any subsequent callers
|
|
18
|
-
* skip the drain wait and emit a warning instead of racing.
|
|
19
|
-
*/
|
|
445
|
+
_tools = [];
|
|
446
|
+
/** Cached tool list — survives reconnects so the registry can re-register without re-discovering. */
|
|
447
|
+
_toolsCache;
|
|
20
448
|
_drainPending = false;
|
|
21
|
-
/** Set when a notify() call failed for reasons the caller should know about. */
|
|
22
449
|
_lastNotifySkipped = false;
|
|
450
|
+
// HTTP transports
|
|
451
|
+
sseTransport;
|
|
452
|
+
httpTransport;
|
|
453
|
+
/** Notified when the stdio child process exits so the registry can attempt reconnect. */
|
|
454
|
+
exitListeners = /* @__PURE__ */ new Set();
|
|
455
|
+
/** Notified when the server announces a tools/list_changed notification. */
|
|
456
|
+
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
457
|
+
/** Notified when an HTTP transport (SSE or streamable-http) disconnects. */
|
|
458
|
+
disconnectListeners = /* @__PURE__ */ new Set();
|
|
23
459
|
getState() {
|
|
24
460
|
return this.state;
|
|
25
461
|
}
|
|
26
462
|
listTools() {
|
|
27
|
-
return [...this.
|
|
463
|
+
return this._tools.length > 0 ? [...this._tools] : this._toolsCache ? [...this._toolsCache] : [];
|
|
28
464
|
}
|
|
29
465
|
/** Returns true if a prior notify() call was skipped due to backpressure. */
|
|
30
466
|
hadNotifySkipped() {
|
|
31
467
|
return this._lastNotifySkipped;
|
|
32
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Register a listener for child-process exit events.
|
|
471
|
+
* The registry uses this to trigger reconnection.
|
|
472
|
+
*/
|
|
473
|
+
addExitListener(listener) {
|
|
474
|
+
this.exitListeners.add(listener);
|
|
475
|
+
}
|
|
476
|
+
removeExitListener(listener) {
|
|
477
|
+
this.exitListeners.delete(listener);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Register a listener for transport disconnect events (SSE / streamable-http).
|
|
481
|
+
* Used by the registry to trigger reconnection for HTTP-based servers.
|
|
482
|
+
*/
|
|
483
|
+
addDisconnectListener(listener) {
|
|
484
|
+
this.disconnectListeners.add(listener);
|
|
485
|
+
}
|
|
486
|
+
removeDisconnectListener(listener) {
|
|
487
|
+
this.disconnectListeners.delete(listener);
|
|
488
|
+
}
|
|
33
489
|
async connect() {
|
|
34
490
|
this.state = "connecting";
|
|
35
|
-
if (this.opts.transport
|
|
491
|
+
if (this.opts.transport === "stdio") {
|
|
492
|
+
await this.connectStdio();
|
|
493
|
+
} else if (this.opts.transport === "sse") {
|
|
494
|
+
await this.connectSSE();
|
|
495
|
+
} else if (this.opts.transport === "streamable-http") {
|
|
496
|
+
await this.connectStreamableHTTP();
|
|
497
|
+
} else {
|
|
36
498
|
this.state = "failed";
|
|
37
|
-
throw new Error(`
|
|
499
|
+
throw new Error(`Unknown transport "${this.opts.transport}"`);
|
|
38
500
|
}
|
|
501
|
+
}
|
|
502
|
+
async connectStdio() {
|
|
39
503
|
if (!this.opts.command) {
|
|
40
504
|
this.state = "failed";
|
|
41
505
|
throw new Error('MCP stdio transport requires "command"');
|
|
@@ -48,8 +512,14 @@ var MCPClient = class {
|
|
|
48
512
|
child.stdout?.on("data", (chunk) => this.onData(chunk.toString()));
|
|
49
513
|
child.stderr?.on("data", () => {
|
|
50
514
|
});
|
|
51
|
-
child.on("exit", () => {
|
|
515
|
+
child.on("exit", (code, signal) => {
|
|
52
516
|
this.state = "disconnected";
|
|
517
|
+
for (const listener of this.exitListeners) {
|
|
518
|
+
try {
|
|
519
|
+
listener(this.opts.name, code, signal);
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
53
523
|
});
|
|
54
524
|
child.on("error", () => {
|
|
55
525
|
this.state = "failed";
|
|
@@ -78,17 +548,104 @@ var MCPClient = class {
|
|
|
78
548
|
}
|
|
79
549
|
const toolsRes = await this.request("tools/list", {});
|
|
80
550
|
if (toolsRes.error) {
|
|
81
|
-
this.
|
|
551
|
+
this._tools = [];
|
|
82
552
|
} else {
|
|
83
553
|
const result = toolsRes.result;
|
|
84
|
-
this.
|
|
554
|
+
this._tools = result?.tools ?? [];
|
|
555
|
+
}
|
|
556
|
+
this._toolsCache = this._tools;
|
|
557
|
+
this.state = "connected";
|
|
558
|
+
}
|
|
559
|
+
async connectSSE() {
|
|
560
|
+
if (!this.opts.url) {
|
|
561
|
+
this.state = "failed";
|
|
562
|
+
throw new Error('MCP SSE transport requires "url"');
|
|
563
|
+
}
|
|
564
|
+
const httpOpts = {
|
|
565
|
+
name: this.opts.name,
|
|
566
|
+
url: this.opts.url,
|
|
567
|
+
headers: this.opts.headers,
|
|
568
|
+
startupTimeoutMs: this.opts.startupTimeoutMs
|
|
569
|
+
};
|
|
570
|
+
this.sseTransport = new SSETransport(httpOpts);
|
|
571
|
+
this.sseTransport.onDisconnect(() => {
|
|
572
|
+
this.state = "disconnected";
|
|
573
|
+
for (const cb of this.disconnectListeners) {
|
|
574
|
+
try {
|
|
575
|
+
cb();
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
this.sseTransport.onToolsChanged((tools) => {
|
|
581
|
+
this._tools = tools;
|
|
582
|
+
for (const cb of this.toolsChangedListeners) {
|
|
583
|
+
try {
|
|
584
|
+
cb(this.opts.name, tools);
|
|
585
|
+
} catch {
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
try {
|
|
590
|
+
await this.sseTransport.connect();
|
|
591
|
+
} catch (err) {
|
|
592
|
+
this.state = "failed";
|
|
593
|
+
throw err;
|
|
594
|
+
}
|
|
595
|
+
this._tools = this.sseTransport.listTools();
|
|
596
|
+
this._toolsCache = this._tools;
|
|
597
|
+
this.state = "connected";
|
|
598
|
+
}
|
|
599
|
+
async connectStreamableHTTP() {
|
|
600
|
+
if (!this.opts.url) {
|
|
601
|
+
this.state = "failed";
|
|
602
|
+
throw new Error('MCP streamable-http transport requires "url"');
|
|
603
|
+
}
|
|
604
|
+
const httpOpts = {
|
|
605
|
+
name: this.opts.name,
|
|
606
|
+
url: this.opts.url,
|
|
607
|
+
headers: this.opts.headers,
|
|
608
|
+
startupTimeoutMs: this.opts.startupTimeoutMs
|
|
609
|
+
};
|
|
610
|
+
this.httpTransport = new StreamableHTTPTransport(httpOpts);
|
|
611
|
+
this.httpTransport.onDisconnect(() => {
|
|
612
|
+
this.state = "disconnected";
|
|
613
|
+
for (const cb of this.disconnectListeners) {
|
|
614
|
+
try {
|
|
615
|
+
cb();
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
this.httpTransport.onToolsChanged((tools) => {
|
|
621
|
+
this._tools = tools;
|
|
622
|
+
for (const cb of this.toolsChangedListeners) {
|
|
623
|
+
try {
|
|
624
|
+
cb(this.opts.name, tools);
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
try {
|
|
630
|
+
await this.httpTransport.connect();
|
|
631
|
+
} catch (err) {
|
|
632
|
+
this.state = "failed";
|
|
633
|
+
throw err;
|
|
85
634
|
}
|
|
635
|
+
this._tools = this.httpTransport.listTools();
|
|
636
|
+
this._toolsCache = this._tools;
|
|
86
637
|
this.state = "connected";
|
|
87
638
|
}
|
|
88
639
|
async callTool(name, input) {
|
|
89
640
|
if (this.state !== "connected") {
|
|
90
641
|
throw new Error(`MCP client "${this.opts.name}" not connected (state=${this.state})`);
|
|
91
642
|
}
|
|
643
|
+
if (this.sseTransport) {
|
|
644
|
+
return this.sseTransport.callTool(name, input);
|
|
645
|
+
}
|
|
646
|
+
if (this.httpTransport) {
|
|
647
|
+
return this.httpTransport.callTool(name, input);
|
|
648
|
+
}
|
|
92
649
|
const res = await this.request("tools/call", { name, arguments: input });
|
|
93
650
|
if (res.error) {
|
|
94
651
|
return { content: res.error.message, isError: true };
|
|
@@ -101,11 +658,19 @@ var MCPClient = class {
|
|
|
101
658
|
}
|
|
102
659
|
async close() {
|
|
103
660
|
if (this.child) {
|
|
661
|
+
const child = this.child;
|
|
662
|
+
const exitPromise = child.exitCode === null && child.signalCode === null ? new Promise((resolve) => child.once("exit", () => resolve())) : Promise.resolve();
|
|
104
663
|
try {
|
|
105
|
-
|
|
664
|
+
child.kill();
|
|
106
665
|
} catch {
|
|
107
666
|
}
|
|
667
|
+
await Promise.race([
|
|
668
|
+
exitPromise,
|
|
669
|
+
new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
670
|
+
]);
|
|
108
671
|
}
|
|
672
|
+
this.sseTransport?.close();
|
|
673
|
+
this.httpTransport?.close();
|
|
109
674
|
this.state = "disconnected";
|
|
110
675
|
}
|
|
111
676
|
request(method, params) {
|
|
@@ -177,12 +742,58 @@ var MCPClient = class {
|
|
|
177
742
|
const resolve = this.pending.get(msg.id);
|
|
178
743
|
this.pending.delete(msg.id);
|
|
179
744
|
resolve?.(msg);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (typeof msg.method === "string" && msg.method === "notifications/tools/list_changed") {
|
|
748
|
+
void this.handleToolsListChanged();
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* L2-C: refresh the cached tool list when the server announces a
|
|
753
|
+
* `tools/list_changed`. Listeners (the registry) re-wrap and
|
|
754
|
+
* re-register. Failures are swallowed — a stale cache is preferable
|
|
755
|
+
* to a hard crash on a transient notification glitch.
|
|
756
|
+
*/
|
|
757
|
+
async handleToolsListChanged() {
|
|
758
|
+
try {
|
|
759
|
+
const toolsRes = await this.request("tools/list", {});
|
|
760
|
+
const tools = (toolsRes.result?.tools ?? []).filter(
|
|
761
|
+
(t) => !!t && typeof t.name === "string"
|
|
762
|
+
);
|
|
763
|
+
this._tools = tools;
|
|
764
|
+
this._toolsCache = tools;
|
|
765
|
+
for (const listener of this.toolsChangedListeners) {
|
|
766
|
+
try {
|
|
767
|
+
listener(this.opts.name, [...tools]);
|
|
768
|
+
} catch {
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} catch {
|
|
180
772
|
}
|
|
181
773
|
}
|
|
774
|
+
addToolsChangedListener(listener) {
|
|
775
|
+
this.toolsChangedListeners.add(listener);
|
|
776
|
+
}
|
|
777
|
+
removeToolsChangedListener(listener) {
|
|
778
|
+
this.toolsChangedListeners.delete(listener);
|
|
779
|
+
}
|
|
182
780
|
};
|
|
183
781
|
|
|
184
782
|
// src/wrap-tool.ts
|
|
185
783
|
var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
|
|
784
|
+
function isMutatingTool(mcpTool) {
|
|
785
|
+
if (MUTATING_RE.test(mcpTool.name)) return true;
|
|
786
|
+
const schema = mcpTool.inputSchema;
|
|
787
|
+
if (schema && typeof schema === "object") {
|
|
788
|
+
const props = schema.properties;
|
|
789
|
+
if (props) {
|
|
790
|
+
for (const key of Object.keys(props)) {
|
|
791
|
+
if (MUTATING_RE.test(key)) return true;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
186
797
|
function wrapMCPTool(serverName, mcpTool, client, permission = "confirm") {
|
|
187
798
|
const qualifiedName = `mcp__${serverName}__${mcpTool.name}`;
|
|
188
799
|
return {
|
|
@@ -190,7 +801,7 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm") {
|
|
|
190
801
|
description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,
|
|
191
802
|
usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
|
|
192
803
|
permission,
|
|
193
|
-
mutating:
|
|
804
|
+
mutating: isMutatingTool(mcpTool),
|
|
194
805
|
inputSchema: mcpTool.inputSchema ?? { type: "object", properties: {} },
|
|
195
806
|
async execute(input, ctx, opts) {
|
|
196
807
|
const res = await client.callTool(mcpTool.name, input);
|
|
@@ -223,7 +834,7 @@ function stringify(c) {
|
|
|
223
834
|
}
|
|
224
835
|
|
|
225
836
|
// src/registry.ts
|
|
226
|
-
var MCPRegistry = class {
|
|
837
|
+
var MCPRegistry = class _MCPRegistry {
|
|
227
838
|
servers = /* @__PURE__ */ new Map();
|
|
228
839
|
toolRegistry;
|
|
229
840
|
events;
|
|
@@ -239,7 +850,9 @@ var MCPRegistry = class {
|
|
|
239
850
|
cfg,
|
|
240
851
|
state: "idle",
|
|
241
852
|
toolNames: [],
|
|
242
|
-
attempts: 0
|
|
853
|
+
attempts: 0,
|
|
854
|
+
reconnectPending: false,
|
|
855
|
+
reconnectCycles: 0
|
|
243
856
|
};
|
|
244
857
|
this.servers.set(cfg.name, slot);
|
|
245
858
|
await this.attemptConnect(slot);
|
|
@@ -247,7 +860,13 @@ var MCPRegistry = class {
|
|
|
247
860
|
async stop(name) {
|
|
248
861
|
const slot = this.servers.get(name);
|
|
249
862
|
if (!slot) return;
|
|
250
|
-
|
|
863
|
+
slot.reconnectPending = false;
|
|
864
|
+
if (slot.client) {
|
|
865
|
+
slot.client.removeExitListener(this.onChildExit);
|
|
866
|
+
slot.client.removeDisconnectListener(() => this.onTransportDisconnect(slot.cfg.name));
|
|
867
|
+
slot.client.removeToolsChangedListener(this.onToolsChanged);
|
|
868
|
+
await slot.client.close();
|
|
869
|
+
}
|
|
251
870
|
for (const t of slot.toolNames) this.toolRegistry.unregister(t);
|
|
252
871
|
slot.toolNames = [];
|
|
253
872
|
slot.state = "disconnected";
|
|
@@ -258,6 +877,7 @@ var MCPRegistry = class {
|
|
|
258
877
|
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
259
878
|
await this.stop(name);
|
|
260
879
|
slot.attempts = 0;
|
|
880
|
+
slot.reconnectCycles = 0;
|
|
261
881
|
await this.attemptConnect(slot);
|
|
262
882
|
}
|
|
263
883
|
list() {
|
|
@@ -272,6 +892,117 @@ var MCPRegistry = class {
|
|
|
272
892
|
await this.stop(name);
|
|
273
893
|
}
|
|
274
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Health check — returns 'ok' for connected servers, the current state otherwise.
|
|
897
|
+
* For HTTP-based transports this could also ping the server.
|
|
898
|
+
*/
|
|
899
|
+
health() {
|
|
900
|
+
return Array.from(this.servers.values()).map((s) => ({
|
|
901
|
+
name: s.cfg.name,
|
|
902
|
+
alive: s.state === "connected"
|
|
903
|
+
}));
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* L2-C: handle `notifications/tools/list_changed` from the server.
|
|
907
|
+
* Unregister the previous wrapper set, then re-register the fresh
|
|
908
|
+
* tool list. The client has already refreshed its cache before
|
|
909
|
+
* dispatching — we just need to re-wrap and re-register.
|
|
910
|
+
*/
|
|
911
|
+
onToolsChanged = (name, _tools) => {
|
|
912
|
+
const slot = this.servers.get(name);
|
|
913
|
+
if (!slot || !slot.client) return;
|
|
914
|
+
for (const t of slot.toolNames) {
|
|
915
|
+
try {
|
|
916
|
+
this.toolRegistry.unregister(t);
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
slot.toolNames = [];
|
|
921
|
+
const allowed = slot.cfg.allowedTools;
|
|
922
|
+
const wrapped = slot.client.listTools().filter((t) => !allowed || allowed.includes(t.name)).map((t) => wrapMCPTool(slot.cfg.name, t, slot.client, slot.cfg.permission ?? "confirm"));
|
|
923
|
+
for (const tool of wrapped) {
|
|
924
|
+
try {
|
|
925
|
+
this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
|
|
926
|
+
slot.toolNames.push(tool.name);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
this.log.warn(`MCP tool "${tool.name}" not re-registered after list_changed`, err);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
this.events.emit("mcp.server.connected", {
|
|
932
|
+
name: slot.cfg.name,
|
|
933
|
+
toolCount: slot.toolNames.length
|
|
934
|
+
});
|
|
935
|
+
this.log.info(
|
|
936
|
+
`MCP server "${slot.cfg.name}" tools refreshed (${slot.toolNames.length} active)`
|
|
937
|
+
);
|
|
938
|
+
};
|
|
939
|
+
onChildExit = (name, code, _signal) => {
|
|
940
|
+
const slot = this.servers.get(name);
|
|
941
|
+
if (!slot) return;
|
|
942
|
+
for (const t of slot.toolNames) {
|
|
943
|
+
try {
|
|
944
|
+
this.toolRegistry.unregister(t);
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
slot.toolNames = [];
|
|
949
|
+
slot.state = "disconnected";
|
|
950
|
+
this.events.emit("mcp.server.disconnected", { name, reason: `exit:${code ?? "unknown"}` });
|
|
951
|
+
this.scheduleReconnect(slot);
|
|
952
|
+
};
|
|
953
|
+
/** Handles SSE / streamable-http disconnect — same recovery as stdio child exit. */
|
|
954
|
+
onTransportDisconnect = (name) => {
|
|
955
|
+
const slot = this.servers.get(name);
|
|
956
|
+
if (!slot) return;
|
|
957
|
+
for (const t of slot.toolNames) {
|
|
958
|
+
try {
|
|
959
|
+
this.toolRegistry.unregister(t);
|
|
960
|
+
} catch {
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
slot.toolNames = [];
|
|
964
|
+
slot.state = "disconnected";
|
|
965
|
+
this.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect" });
|
|
966
|
+
this.scheduleReconnect(slot);
|
|
967
|
+
};
|
|
968
|
+
/**
|
|
969
|
+
* L2-B: maximum number of reconnect cycles before staying `failed`.
|
|
970
|
+
* One cycle = one full `attemptConnect` (which itself may try up to 3
|
|
971
|
+
* times). Caps total reconnect storm at ~5 cycles, then the slot
|
|
972
|
+
* needs an explicit `restart()` to re-engage.
|
|
973
|
+
*/
|
|
974
|
+
static MAX_RECONNECT_CYCLES = 5;
|
|
975
|
+
/** Base delay between cycles, in ms. Real delay adds jitter. */
|
|
976
|
+
static BASE_RECONNECT_DELAY_MS = 1e3;
|
|
977
|
+
/** Hard ceiling on the inter-cycle delay so the user doesn't wait minutes. */
|
|
978
|
+
static MAX_RECONNECT_DELAY_MS = 3e4;
|
|
979
|
+
scheduleReconnect(slot) {
|
|
980
|
+
if (slot.reconnectPending) return;
|
|
981
|
+
if (slot.reconnectCycles >= _MCPRegistry.MAX_RECONNECT_CYCLES) {
|
|
982
|
+
slot.state = "failed";
|
|
983
|
+
this.log.error(
|
|
984
|
+
`MCP server "${slot.cfg.name}" giving up after ${slot.reconnectCycles} reconnect cycles. Use \`/mcp restart ${slot.cfg.name}\` to retry.`
|
|
985
|
+
);
|
|
986
|
+
this.events.emit("mcp.server.disconnected", {
|
|
987
|
+
name: slot.cfg.name,
|
|
988
|
+
reason: `reconnect-exhausted:${slot.reconnectCycles}`
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
slot.reconnectPending = true;
|
|
993
|
+
const base = Math.min(
|
|
994
|
+
_MCPRegistry.BASE_RECONNECT_DELAY_MS * 2 ** slot.reconnectCycles,
|
|
995
|
+
_MCPRegistry.MAX_RECONNECT_DELAY_MS
|
|
996
|
+
);
|
|
997
|
+
const jitter = base * 0.2 * (Math.random() * 2 - 1);
|
|
998
|
+
const delay = Math.max(100, Math.round(base + jitter));
|
|
999
|
+
setTimeout(() => this.attemptReconnect(slot), delay);
|
|
1000
|
+
}
|
|
1001
|
+
async attemptReconnect(slot) {
|
|
1002
|
+
slot.reconnectPending = false;
|
|
1003
|
+
slot.reconnectCycles++;
|
|
1004
|
+
await this.attemptConnect(slot);
|
|
1005
|
+
}
|
|
275
1006
|
async attemptConnect(slot) {
|
|
276
1007
|
const MAX_ATTEMPTS = 3;
|
|
277
1008
|
let attempt = 0;
|
|
@@ -279,8 +1010,9 @@ var MCPRegistry = class {
|
|
|
279
1010
|
attempt++;
|
|
280
1011
|
slot.state = attempt === 1 ? "connecting" : "reconnecting";
|
|
281
1012
|
slot.attempts = attempt;
|
|
1013
|
+
let client;
|
|
282
1014
|
try {
|
|
283
|
-
|
|
1015
|
+
client = new MCPClient({
|
|
284
1016
|
name: slot.cfg.name,
|
|
285
1017
|
transport: slot.cfg.transport,
|
|
286
1018
|
command: slot.cfg.command,
|
|
@@ -290,12 +1022,22 @@ var MCPRegistry = class {
|
|
|
290
1022
|
headers: slot.cfg.headers,
|
|
291
1023
|
startupTimeoutMs: slot.cfg.startupTimeoutMs
|
|
292
1024
|
});
|
|
1025
|
+
if (slot.cfg.transport === "stdio") {
|
|
1026
|
+
client.addExitListener(this.onChildExit);
|
|
1027
|
+
} else {
|
|
1028
|
+
client.addDisconnectListener(() => this.onTransportDisconnect(slot.cfg.name));
|
|
1029
|
+
}
|
|
1030
|
+
client.addToolsChangedListener(this.onToolsChanged);
|
|
293
1031
|
await client.connect();
|
|
294
1032
|
slot.client = client;
|
|
295
1033
|
const isReconnect = attempt > 1;
|
|
296
1034
|
slot.state = "connected";
|
|
1035
|
+
slot.reconnectCycles = 0;
|
|
297
1036
|
const allowed = slot.cfg.allowedTools;
|
|
298
|
-
const
|
|
1037
|
+
const mc = client;
|
|
1038
|
+
const candidateTools = mc.listTools();
|
|
1039
|
+
const toWrap = candidateTools.length > 0 ? candidateTools : mc.listTools();
|
|
1040
|
+
const wrapped = toWrap.filter((t) => !allowed || allowed.includes(t.name)).map((t) => wrapMCPTool(slot.cfg.name, t, mc, slot.cfg.permission ?? "confirm"));
|
|
299
1041
|
for (const tool of wrapped) {
|
|
300
1042
|
try {
|
|
301
1043
|
this.toolRegistry.register(tool, `mcp:${slot.cfg.name}`);
|
|
@@ -311,9 +1053,17 @@ var MCPRegistry = class {
|
|
|
311
1053
|
return;
|
|
312
1054
|
} catch (err) {
|
|
313
1055
|
this.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
1056
|
+
if (client) {
|
|
1057
|
+
client.removeExitListener(this.onChildExit);
|
|
1058
|
+
client.removeDisconnectListener(() => this.onTransportDisconnect(slot.cfg.name));
|
|
1059
|
+
client.removeToolsChangedListener(this.onToolsChanged);
|
|
1060
|
+
await client.close().catch(() => {
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
314
1063
|
if (attempt >= MAX_ATTEMPTS) {
|
|
315
1064
|
this.log.error(`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS} attempts`, err);
|
|
316
1065
|
slot.state = "failed";
|
|
1066
|
+
slot.client = void 0;
|
|
317
1067
|
this.events.emit("mcp.server.disconnected", {
|
|
318
1068
|
name: slot.cfg.name,
|
|
319
1069
|
reason: err instanceof Error ? err.message : "unknown"
|
|
@@ -327,6 +1077,6 @@ var MCPRegistry = class {
|
|
|
327
1077
|
}
|
|
328
1078
|
};
|
|
329
1079
|
|
|
330
|
-
export { MCPClient, MCPRegistry, wrapMCPTool };
|
|
1080
|
+
export { MCPClient, MCPRegistry, SSEReader, SSETransport, StreamableHTTPTransport, wrapMCPTool };
|
|
331
1081
|
//# sourceMappingURL=index.js.map
|
|
332
1082
|
//# sourceMappingURL=index.js.map
|