@ai-ecoverse/mcpcat 0.0.0 → 0.2.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/LICENSE +190 -0
- package/README.md +19 -5
- package/dist/host/mcpcat.mjs +2626 -0
- package/dist/kernel/mcpcat.mjs +2038 -0
- package/package.json +59 -2
|
@@ -0,0 +1,2038 @@
|
|
|
1
|
+
// node_modules/@earendil-works/pi-mcp/dist/protocol/jsonrpc.js
|
|
2
|
+
var JSON_RPC_ERROR_CODES = {
|
|
3
|
+
parseError: -32700,
|
|
4
|
+
invalidRequest: -32600,
|
|
5
|
+
methodNotFound: -32601,
|
|
6
|
+
invalidParams: -32602,
|
|
7
|
+
internalError: -32603
|
|
8
|
+
};
|
|
9
|
+
var McpError = class extends Error {
|
|
10
|
+
code;
|
|
11
|
+
data;
|
|
12
|
+
constructor(code, message, data) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "McpError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.data = data;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var McpConnectionClosedError = class extends Error {
|
|
20
|
+
constructor(message = "MCP connection closed") {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "McpConnectionClosedError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var McpTimeoutError = class extends Error {
|
|
26
|
+
timeoutMs;
|
|
27
|
+
constructor(timeoutMs) {
|
|
28
|
+
super(`MCP request timed out after ${timeoutMs}ms`);
|
|
29
|
+
this.name = "McpTimeoutError";
|
|
30
|
+
this.timeoutMs = timeoutMs;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var McpAbortError = class extends Error {
|
|
34
|
+
constructor(message = "MCP request aborted") {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "AbortError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function isObject(value) {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
function toError(value) {
|
|
43
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
44
|
+
}
|
|
45
|
+
function isJsonRpcId(value) {
|
|
46
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
47
|
+
}
|
|
48
|
+
function isJsonRpcRequest(message) {
|
|
49
|
+
return isObject(message) && message.jsonrpc === "2.0" && isJsonRpcId(message.id) && typeof message.method === "string";
|
|
50
|
+
}
|
|
51
|
+
function isJsonRpcNotification(message) {
|
|
52
|
+
return isObject(message) && message.jsonrpc === "2.0" && !("id" in message) && typeof message.method === "string";
|
|
53
|
+
}
|
|
54
|
+
function isJsonRpcResponse(message) {
|
|
55
|
+
if (!isObject(message) || message.jsonrpc !== "2.0" || !isJsonRpcId(message.id))
|
|
56
|
+
return false;
|
|
57
|
+
if ("result" in message)
|
|
58
|
+
return !("error" in message);
|
|
59
|
+
if (!("error" in message) || !isObject(message.error))
|
|
60
|
+
return false;
|
|
61
|
+
return typeof message.error.code === "number" && typeof message.error.message === "string";
|
|
62
|
+
}
|
|
63
|
+
function parseJsonRpcMessage(value) {
|
|
64
|
+
if (isJsonRpcRequest(value) || isJsonRpcNotification(value) || isJsonRpcResponse(value))
|
|
65
|
+
return value;
|
|
66
|
+
throw new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid JSON-RPC message");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// node_modules/@earendil-works/pi-mcp/dist/protocol/types.js
|
|
70
|
+
var LATEST_PROTOCOL_VERSION = "2025-11-25";
|
|
71
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
72
|
+
|
|
73
|
+
// node_modules/@earendil-works/pi-mcp/dist/client.js
|
|
74
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
75
|
+
var MAX_LIST_PAGES = 1e3;
|
|
76
|
+
function validateInitializeResult(value) {
|
|
77
|
+
if (!isObject(value) || typeof value.protocolVersion !== "string" || !isObject(value.capabilities) || !isObject(value.serverInfo) || typeof value.serverInfo.name !== "string" || typeof value.serverInfo.version !== "string" || value.instructions !== void 0 && typeof value.instructions !== "string") {
|
|
78
|
+
throw new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid MCP initialize result");
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function invalid(message) {
|
|
83
|
+
return new McpError(JSON_RPC_ERROR_CODES.invalidRequest, message);
|
|
84
|
+
}
|
|
85
|
+
function validateListPage(method, key, value, isItem) {
|
|
86
|
+
const items = isObject(value) ? value[key] : void 0;
|
|
87
|
+
if (!isObject(value) || !Array.isArray(items))
|
|
88
|
+
throw invalid(`Invalid MCP ${method} result`);
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
if (!isObject(item) || !isItem(item))
|
|
91
|
+
throw invalid(`Invalid entry in MCP ${method} result`);
|
|
92
|
+
}
|
|
93
|
+
const nextCursor = value.nextCursor === null || value.nextCursor === "" ? void 0 : value.nextCursor;
|
|
94
|
+
if (nextCursor !== void 0 && typeof nextCursor !== "string")
|
|
95
|
+
throw invalid(`Invalid MCP ${method} cursor`);
|
|
96
|
+
return { items, ...nextCursor === void 0 ? {} : { nextCursor } };
|
|
97
|
+
}
|
|
98
|
+
var isTool = (tool) => typeof tool.name === "string" && isObject(tool.inputSchema);
|
|
99
|
+
var isResource = (resource) => typeof resource.uri === "string" && (resource.name === void 0 || typeof resource.name === "string");
|
|
100
|
+
var isResourceTemplate = (template) => typeof template.uriTemplate === "string" && (template.name === void 0 || typeof template.name === "string");
|
|
101
|
+
function toResource(item) {
|
|
102
|
+
return { ...item, name: item.name ?? item.uri };
|
|
103
|
+
}
|
|
104
|
+
function toResourceTemplate(item) {
|
|
105
|
+
return { ...item, name: item.name ?? item.uriTemplate };
|
|
106
|
+
}
|
|
107
|
+
function pageCursor(page) {
|
|
108
|
+
return page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor };
|
|
109
|
+
}
|
|
110
|
+
function validateReadResourceResult(value) {
|
|
111
|
+
if (!isObject(value) || !Array.isArray(value.contents))
|
|
112
|
+
throw invalid("Invalid MCP resources/read result");
|
|
113
|
+
for (const contents of value.contents) {
|
|
114
|
+
if (!isObject(contents) || typeof contents.uri !== "string" || typeof contents.text !== "string" && typeof contents.blob !== "string") {
|
|
115
|
+
throw invalid("Invalid contents in MCP resources/read result");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function validateCallToolResult(value) {
|
|
121
|
+
if (!isObject(value) || value.content !== void 0 && !Array.isArray(value.content)) {
|
|
122
|
+
throw new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid MCP tools/call result");
|
|
123
|
+
}
|
|
124
|
+
return value.content === void 0 ? { ...value, content: [] } : value;
|
|
125
|
+
}
|
|
126
|
+
var McpClient = class {
|
|
127
|
+
options;
|
|
128
|
+
state = "idle";
|
|
129
|
+
transport;
|
|
130
|
+
nextRequestId = 1;
|
|
131
|
+
serverInfoValue;
|
|
132
|
+
serverCapabilitiesValue;
|
|
133
|
+
instructionsValue;
|
|
134
|
+
protocolVersionValue;
|
|
135
|
+
pending = /* @__PURE__ */ new Map();
|
|
136
|
+
progressRequests = /* @__PURE__ */ new Map();
|
|
137
|
+
incoming = /* @__PURE__ */ new Map();
|
|
138
|
+
requestHandlers = /* @__PURE__ */ new Map();
|
|
139
|
+
notificationListeners = /* @__PURE__ */ new Map();
|
|
140
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
141
|
+
closeListeners = /* @__PURE__ */ new Set();
|
|
142
|
+
disposers = [];
|
|
143
|
+
constructor(options) {
|
|
144
|
+
this.options = Object.freeze({ ...options });
|
|
145
|
+
this.requestHandlers.set("ping", () => ({}));
|
|
146
|
+
const roots = options.roots;
|
|
147
|
+
if (roots) {
|
|
148
|
+
this.requestHandlers.set("roots/list", async () => ({
|
|
149
|
+
roots: [...typeof roots === "function" ? await roots() : roots]
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
get connectionState() {
|
|
154
|
+
return this.state;
|
|
155
|
+
}
|
|
156
|
+
get serverInfo() {
|
|
157
|
+
return this.serverInfoValue;
|
|
158
|
+
}
|
|
159
|
+
get serverCapabilities() {
|
|
160
|
+
return this.serverCapabilitiesValue;
|
|
161
|
+
}
|
|
162
|
+
get instructions() {
|
|
163
|
+
return this.instructionsValue;
|
|
164
|
+
}
|
|
165
|
+
get protocolVersion() {
|
|
166
|
+
return this.protocolVersionValue;
|
|
167
|
+
}
|
|
168
|
+
async connect(transport) {
|
|
169
|
+
if (this.state !== "idle")
|
|
170
|
+
throw new Error(`Cannot connect MCP client in ${this.state} state`);
|
|
171
|
+
this.state = "connecting";
|
|
172
|
+
this.transport = transport;
|
|
173
|
+
this.disposers = [
|
|
174
|
+
transport.onMessage((message) => this.handleMessage(message)),
|
|
175
|
+
// Transport errors are reported only. Pending requests fail when the transport closes.
|
|
176
|
+
transport.onError((error) => this.emitError(error)),
|
|
177
|
+
transport.onClose(() => this.handleTransportClose())
|
|
178
|
+
];
|
|
179
|
+
try {
|
|
180
|
+
await transport.start();
|
|
181
|
+
const capabilities = { ...this.options.capabilities };
|
|
182
|
+
if (this.options.roots && capabilities.roots === void 0)
|
|
183
|
+
capabilities.roots = {};
|
|
184
|
+
const result = validateInitializeResult(await this.requestInternal("initialize", {
|
|
185
|
+
protocolVersion: this.options.protocolVersion ?? LATEST_PROTOCOL_VERSION,
|
|
186
|
+
capabilities,
|
|
187
|
+
clientInfo: {
|
|
188
|
+
name: this.options.name,
|
|
189
|
+
version: this.options.version,
|
|
190
|
+
...this.options.title === void 0 ? {} : { title: this.options.title }
|
|
191
|
+
}
|
|
192
|
+
}, {}, true));
|
|
193
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
194
|
+
throw new Error(`MCP server selected unsupported protocol version ${result.protocolVersion}`);
|
|
195
|
+
}
|
|
196
|
+
this.protocolVersionValue = result.protocolVersion;
|
|
197
|
+
this.serverInfoValue = result.serverInfo;
|
|
198
|
+
this.serverCapabilitiesValue = result.capabilities;
|
|
199
|
+
this.instructionsValue = result.instructions;
|
|
200
|
+
transport.setProtocolVersion?.(result.protocolVersion);
|
|
201
|
+
await this.notifyInternal("notifications/initialized", void 0, true);
|
|
202
|
+
this.state = "connected";
|
|
203
|
+
return result;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await this.close().catch(() => {
|
|
206
|
+
});
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
request(method, params, options = {}) {
|
|
211
|
+
return this.requestInternal(method, params, options, false);
|
|
212
|
+
}
|
|
213
|
+
notify(method, params) {
|
|
214
|
+
return this.notifyInternal(method, params, false);
|
|
215
|
+
}
|
|
216
|
+
setRequestHandler(method, handler) {
|
|
217
|
+
this.requestHandlers.set(method, handler);
|
|
218
|
+
return () => {
|
|
219
|
+
if (this.requestHandlers.get(method) === handler)
|
|
220
|
+
this.requestHandlers.delete(method);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
onNotification(method, listener) {
|
|
224
|
+
const listeners = this.notificationListeners.get(method) ?? /* @__PURE__ */ new Set();
|
|
225
|
+
this.notificationListeners.set(method, listeners);
|
|
226
|
+
listeners.add(listener);
|
|
227
|
+
return () => {
|
|
228
|
+
listeners.delete(listener);
|
|
229
|
+
if (listeners.size === 0)
|
|
230
|
+
this.notificationListeners.delete(method);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
onError(listener) {
|
|
234
|
+
this.errorListeners.add(listener);
|
|
235
|
+
return () => this.errorListeners.delete(listener);
|
|
236
|
+
}
|
|
237
|
+
/** Called once when the connection closes, whether the transport dropped or `close()` was called. */
|
|
238
|
+
onClose(listener) {
|
|
239
|
+
this.closeListeners.add(listener);
|
|
240
|
+
return () => this.closeListeners.delete(listener);
|
|
241
|
+
}
|
|
242
|
+
async ping(options = {}) {
|
|
243
|
+
await this.request("ping", void 0, options);
|
|
244
|
+
}
|
|
245
|
+
async listTools(options = {}) {
|
|
246
|
+
return await this.listAll("tools/list", "tools", isTool, options);
|
|
247
|
+
}
|
|
248
|
+
/** Every resource, following `nextCursor` through all pages. */
|
|
249
|
+
async listResources(options = {}) {
|
|
250
|
+
return (await this.listAll("resources/list", "resources", isResource, options)).map(toResource);
|
|
251
|
+
}
|
|
252
|
+
/** One page of resources, starting at `cursor`. */
|
|
253
|
+
async listResourcesPage(cursor, options = {}) {
|
|
254
|
+
const page = await this.listPage("resources/list", "resources", isResource, cursor, options);
|
|
255
|
+
return { resources: page.items.map(toResource), ...pageCursor(page) };
|
|
256
|
+
}
|
|
257
|
+
/** Every resource template, following `nextCursor` through all pages. */
|
|
258
|
+
async listResourceTemplates(options = {}) {
|
|
259
|
+
const templates = await this.listAll("resources/templates/list", "resourceTemplates", isResourceTemplate, options);
|
|
260
|
+
return templates.map(toResourceTemplate);
|
|
261
|
+
}
|
|
262
|
+
/** One page of resource templates, starting at `cursor`. */
|
|
263
|
+
async listResourceTemplatesPage(cursor, options = {}) {
|
|
264
|
+
const page = await this.listPage("resources/templates/list", "resourceTemplates", isResourceTemplate, cursor, options);
|
|
265
|
+
return { resourceTemplates: page.items.map(toResourceTemplate), ...pageCursor(page) };
|
|
266
|
+
}
|
|
267
|
+
async readResource(uri, options = {}) {
|
|
268
|
+
return validateReadResourceResult(await this.request("resources/read", { uri }, options));
|
|
269
|
+
}
|
|
270
|
+
async listPage(method, key, isItem, cursor, options) {
|
|
271
|
+
return validateListPage(method, key, await this.request(method, cursor === void 0 ? void 0 : { cursor }, options), isItem);
|
|
272
|
+
}
|
|
273
|
+
/** Every item of a paginated list method. */
|
|
274
|
+
async listAll(method, key, isItem, options) {
|
|
275
|
+
const items = [];
|
|
276
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
277
|
+
let cursor;
|
|
278
|
+
for (let pageNumber = 0; pageNumber < MAX_LIST_PAGES; pageNumber++) {
|
|
279
|
+
const page = await this.listPage(method, key, isItem, cursor, options);
|
|
280
|
+
items.push(...page.items);
|
|
281
|
+
if (page.nextCursor === void 0)
|
|
282
|
+
return items;
|
|
283
|
+
if (cursors.has(page.nextCursor))
|
|
284
|
+
throw new Error(`MCP ${method} returned duplicate cursor: ${page.nextCursor}`);
|
|
285
|
+
cursors.add(page.nextCursor);
|
|
286
|
+
cursor = page.nextCursor;
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`MCP ${method} exceeded ${MAX_LIST_PAGES} pages`);
|
|
289
|
+
}
|
|
290
|
+
async callTool(name, args, options = {}) {
|
|
291
|
+
return validateCallToolResult(await this.request("tools/call", { name, ...args === void 0 ? {} : { arguments: args } }, options));
|
|
292
|
+
}
|
|
293
|
+
async close() {
|
|
294
|
+
const transport = this.transport;
|
|
295
|
+
this.transport = void 0;
|
|
296
|
+
this.disposeTransportListeners();
|
|
297
|
+
this.markClosed(new McpConnectionClosedError());
|
|
298
|
+
await transport?.close();
|
|
299
|
+
}
|
|
300
|
+
async requestInternal(method, params, options, allowConnecting) {
|
|
301
|
+
const transport = this.requireTransport(allowConnecting);
|
|
302
|
+
if (options.signal?.aborted)
|
|
303
|
+
throw new McpAbortError();
|
|
304
|
+
const id = this.nextRequestId++;
|
|
305
|
+
const progressToken = options.onProgress ? id : void 0;
|
|
306
|
+
const requestParams = progressToken === void 0 ? params : { ...params, _meta: { ...isObject(params?._meta) ? params._meta : {}, progressToken } };
|
|
307
|
+
const message = {
|
|
308
|
+
jsonrpc: "2.0",
|
|
309
|
+
id,
|
|
310
|
+
method,
|
|
311
|
+
...requestParams === void 0 ? {} : { params: requestParams }
|
|
312
|
+
};
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
const entry = {
|
|
315
|
+
resolve,
|
|
316
|
+
reject,
|
|
317
|
+
timeoutMs: options.timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
318
|
+
timer: void 0,
|
|
319
|
+
signal: options.signal,
|
|
320
|
+
// The spec forbids cancelling `initialize`.
|
|
321
|
+
onAbort: () => this.cancelPending(id, new McpAbortError(), method !== "initialize", String(options.signal?.reason ?? "Aborted")),
|
|
322
|
+
sendController: new AbortController(),
|
|
323
|
+
cancellable: method !== "initialize",
|
|
324
|
+
onProgress: options.onProgress,
|
|
325
|
+
progressToken
|
|
326
|
+
};
|
|
327
|
+
this.pending.set(id, entry);
|
|
328
|
+
if (progressToken !== void 0)
|
|
329
|
+
this.progressRequests.set(progressToken, id);
|
|
330
|
+
options.signal?.addEventListener("abort", entry.onAbort, { once: true });
|
|
331
|
+
this.armTimeout(id, entry);
|
|
332
|
+
transport.send(message, { signal: entry.sendController.signal }).catch((error) => this.cancelPending(id, error, false));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async notifyInternal(method, params, allowConnecting) {
|
|
336
|
+
await this.requireTransport(allowConnecting).send({
|
|
337
|
+
jsonrpc: "2.0",
|
|
338
|
+
method,
|
|
339
|
+
...params === void 0 ? {} : { params }
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
requireTransport(allowConnecting) {
|
|
343
|
+
if (this.transport && (this.state === "connected" || allowConnecting && this.state === "connecting")) {
|
|
344
|
+
return this.transport;
|
|
345
|
+
}
|
|
346
|
+
throw new McpConnectionClosedError(`MCP client is ${this.state}`);
|
|
347
|
+
}
|
|
348
|
+
handleMessage(message) {
|
|
349
|
+
if (isJsonRpcResponse(message)) {
|
|
350
|
+
this.handleResponse(message);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (isJsonRpcRequest(message)) {
|
|
354
|
+
void this.handleRequest(message);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (isJsonRpcNotification(message)) {
|
|
358
|
+
this.handleNotification(message.method, message.params);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.emitError(new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Received invalid JSON-RPC message"));
|
|
362
|
+
}
|
|
363
|
+
handleResponse(message) {
|
|
364
|
+
const entry = this.pending.get(message.id);
|
|
365
|
+
if (!entry) {
|
|
366
|
+
this.emitError(new Error(`Received response for unknown MCP request ${String(message.id)}`));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.removePending(message.id, entry);
|
|
370
|
+
if ("error" in message)
|
|
371
|
+
entry.reject(new McpError(message.error.code, message.error.message, message.error.data));
|
|
372
|
+
else
|
|
373
|
+
entry.resolve(message.result);
|
|
374
|
+
}
|
|
375
|
+
async handleRequest(message) {
|
|
376
|
+
const transport = this.transport;
|
|
377
|
+
if (!transport)
|
|
378
|
+
return;
|
|
379
|
+
const handler = this.requestHandlers.get(message.method);
|
|
380
|
+
if (!handler) {
|
|
381
|
+
await transport.send({
|
|
382
|
+
jsonrpc: "2.0",
|
|
383
|
+
id: message.id,
|
|
384
|
+
error: { code: JSON_RPC_ERROR_CODES.methodNotFound, message: `Method not found: ${message.method}` }
|
|
385
|
+
}).catch((error) => this.emitError(error));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const controller = new AbortController();
|
|
389
|
+
this.incoming.set(message.id, controller);
|
|
390
|
+
try {
|
|
391
|
+
const result = await handler(message.params, { signal: controller.signal });
|
|
392
|
+
await transport.send({ jsonrpc: "2.0", id: message.id, result: result ?? {} });
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const responseError = error instanceof McpError ? { code: error.code, message: error.message, data: error.data } : { code: JSON_RPC_ERROR_CODES.internalError, message: toError(error).message };
|
|
395
|
+
await transport.send({ jsonrpc: "2.0", id: message.id, error: responseError }).catch((sendError) => this.emitError(sendError));
|
|
396
|
+
} finally {
|
|
397
|
+
this.incoming.delete(message.id);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
handleNotification(method, params) {
|
|
401
|
+
if (method === "notifications/progress")
|
|
402
|
+
this.handleProgress(params);
|
|
403
|
+
else if (method === "notifications/cancelled")
|
|
404
|
+
this.handleCancelled(params);
|
|
405
|
+
for (const listener of this.notificationListeners.get(method) ?? []) {
|
|
406
|
+
try {
|
|
407
|
+
listener(params);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
this.emitError(error);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
handleProgress(params) {
|
|
414
|
+
if (!isObject(params) || !isJsonRpcId(params.progressToken) || typeof params.progress !== "number")
|
|
415
|
+
return;
|
|
416
|
+
const requestId = this.progressRequests.get(params.progressToken);
|
|
417
|
+
const entry = requestId === void 0 ? void 0 : this.pending.get(requestId);
|
|
418
|
+
if (requestId === void 0 || !entry)
|
|
419
|
+
return;
|
|
420
|
+
this.armTimeout(requestId, entry);
|
|
421
|
+
try {
|
|
422
|
+
entry.onProgress?.(params);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
this.emitError(error);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
handleCancelled(params) {
|
|
428
|
+
if (isObject(params) && isJsonRpcId(params.requestId))
|
|
429
|
+
this.incoming.get(params.requestId)?.abort(params.reason);
|
|
430
|
+
}
|
|
431
|
+
armTimeout(id, entry) {
|
|
432
|
+
if (entry.timer)
|
|
433
|
+
clearTimeout(entry.timer);
|
|
434
|
+
if (!Number.isFinite(entry.timeoutMs) || entry.timeoutMs <= 0)
|
|
435
|
+
return;
|
|
436
|
+
entry.timer = setTimeout(() => {
|
|
437
|
+
this.cancelPending(id, new McpTimeoutError(entry.timeoutMs), entry.cancellable, "Request timed out");
|
|
438
|
+
}, entry.timeoutMs);
|
|
439
|
+
}
|
|
440
|
+
cancelPending(id, error, notifyServer, reason) {
|
|
441
|
+
const entry = this.pending.get(id);
|
|
442
|
+
if (!entry)
|
|
443
|
+
return;
|
|
444
|
+
this.removePending(id, entry);
|
|
445
|
+
entry.reject(error);
|
|
446
|
+
if (notifyServer && this.transport) {
|
|
447
|
+
void this.transport.send({
|
|
448
|
+
jsonrpc: "2.0",
|
|
449
|
+
method: "notifications/cancelled",
|
|
450
|
+
params: { requestId: id, ...reason ? { reason } : {} }
|
|
451
|
+
}).catch((sendError) => this.emitError(sendError));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
removePending(id, entry) {
|
|
455
|
+
this.pending.delete(id);
|
|
456
|
+
if (entry.timer)
|
|
457
|
+
clearTimeout(entry.timer);
|
|
458
|
+
if (entry.progressToken !== void 0)
|
|
459
|
+
this.progressRequests.delete(entry.progressToken);
|
|
460
|
+
entry.signal?.removeEventListener("abort", entry.onAbort);
|
|
461
|
+
entry.sendController.abort();
|
|
462
|
+
}
|
|
463
|
+
rejectPending(error) {
|
|
464
|
+
for (const [id, entry] of this.pending) {
|
|
465
|
+
this.removePending(id, entry);
|
|
466
|
+
entry.reject(error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
handleTransportClose() {
|
|
470
|
+
this.markClosed(new McpConnectionClosedError());
|
|
471
|
+
}
|
|
472
|
+
/** Idempotent: rejects in-flight requests, aborts server requests we are serving, and flips the state. */
|
|
473
|
+
markClosed(error) {
|
|
474
|
+
const wasClosed = this.state === "closed";
|
|
475
|
+
this.state = "closed";
|
|
476
|
+
this.rejectPending(error);
|
|
477
|
+
for (const controller of this.incoming.values())
|
|
478
|
+
controller.abort(error);
|
|
479
|
+
this.incoming.clear();
|
|
480
|
+
if (wasClosed)
|
|
481
|
+
return;
|
|
482
|
+
for (const listener of [...this.closeListeners]) {
|
|
483
|
+
try {
|
|
484
|
+
listener();
|
|
485
|
+
} catch (listenerError) {
|
|
486
|
+
this.emitError(listenerError);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
emitError(error) {
|
|
491
|
+
const normalized = toError(error);
|
|
492
|
+
for (const listener of this.errorListeners)
|
|
493
|
+
listener(normalized);
|
|
494
|
+
}
|
|
495
|
+
disposeTransportListeners() {
|
|
496
|
+
for (const dispose of this.disposers.splice(0))
|
|
497
|
+
dispose();
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/stubs/node-stub.ts
|
|
502
|
+
var node_stub_default = {};
|
|
503
|
+
function spawn() {
|
|
504
|
+
throw new Error("not available in kernel");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/transport.js
|
|
508
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 16 * 1024 * 1024;
|
|
509
|
+
var TransportEvents = class {
|
|
510
|
+
messageListeners = /* @__PURE__ */ new Set();
|
|
511
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
512
|
+
closeListeners = /* @__PURE__ */ new Set();
|
|
513
|
+
closeEmitted = false;
|
|
514
|
+
onMessage(listener) {
|
|
515
|
+
this.messageListeners.add(listener);
|
|
516
|
+
return () => this.messageListeners.delete(listener);
|
|
517
|
+
}
|
|
518
|
+
onError(listener) {
|
|
519
|
+
this.errorListeners.add(listener);
|
|
520
|
+
return () => this.errorListeners.delete(listener);
|
|
521
|
+
}
|
|
522
|
+
onClose(listener) {
|
|
523
|
+
this.closeListeners.add(listener);
|
|
524
|
+
return () => this.closeListeners.delete(listener);
|
|
525
|
+
}
|
|
526
|
+
emitMessage(message) {
|
|
527
|
+
for (const listener of this.messageListeners)
|
|
528
|
+
listener(message);
|
|
529
|
+
}
|
|
530
|
+
emitError(error) {
|
|
531
|
+
const normalized = toError(error);
|
|
532
|
+
for (const listener of this.errorListeners)
|
|
533
|
+
listener(normalized);
|
|
534
|
+
}
|
|
535
|
+
emitClose() {
|
|
536
|
+
if (this.closeEmitted)
|
|
537
|
+
return;
|
|
538
|
+
this.closeEmitted = true;
|
|
539
|
+
for (const listener of this.closeListeners)
|
|
540
|
+
listener();
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/stdio.js
|
|
545
|
+
var DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
|
|
546
|
+
var DEFAULT_CLOSE_TIMEOUT_MS = 2e3;
|
|
547
|
+
var STDIN_CLOSE_GRACE_MS = 500;
|
|
548
|
+
var USE_PROCESS_GROUPS = node_stub_default.platform !== "win32";
|
|
549
|
+
var liveProcessGroups = /* @__PURE__ */ new Set();
|
|
550
|
+
var exitHookInstalled = false;
|
|
551
|
+
function killProcessTree(child, signal) {
|
|
552
|
+
const pid = child.pid;
|
|
553
|
+
if (node_stub_default.platform === "win32" && pid !== void 0) {
|
|
554
|
+
if (child.exitCode !== null)
|
|
555
|
+
return;
|
|
556
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }).on("error", () => {
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (USE_PROCESS_GROUPS && pid !== void 0) {
|
|
561
|
+
try {
|
|
562
|
+
node_stub_default.kill(-pid, signal);
|
|
563
|
+
return;
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
child.kill(signal);
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function installExitHook() {
|
|
573
|
+
if (exitHookInstalled)
|
|
574
|
+
return;
|
|
575
|
+
exitHookInstalled = true;
|
|
576
|
+
node_stub_default.once("exit", () => {
|
|
577
|
+
for (const pid of liveProcessGroups) {
|
|
578
|
+
try {
|
|
579
|
+
node_stub_default.kill(-pid, "SIGTERM");
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
var StdioTransport = class extends TransportEvents {
|
|
586
|
+
options;
|
|
587
|
+
child;
|
|
588
|
+
stdoutBuffer = Buffer.alloc(0);
|
|
589
|
+
stderrBuffer = Buffer.alloc(0);
|
|
590
|
+
started = false;
|
|
591
|
+
closed = false;
|
|
592
|
+
constructor(options) {
|
|
593
|
+
super();
|
|
594
|
+
this.options = Object.freeze({ ...options, args: options.args ? [...options.args] : void 0 });
|
|
595
|
+
}
|
|
596
|
+
get pid() {
|
|
597
|
+
return this.child?.pid;
|
|
598
|
+
}
|
|
599
|
+
get stderr() {
|
|
600
|
+
return this.stderrBuffer.toString("utf8");
|
|
601
|
+
}
|
|
602
|
+
async start() {
|
|
603
|
+
if (this.started)
|
|
604
|
+
throw new Error("MCP stdio transport already started");
|
|
605
|
+
if (this.closed)
|
|
606
|
+
throw new McpConnectionClosedError();
|
|
607
|
+
this.started = true;
|
|
608
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env } : { ...node_stub_default.env, ...this.options.env };
|
|
609
|
+
const child = node_stub_default(this.options.command, this.options.args ?? [], {
|
|
610
|
+
cwd: this.options.cwd,
|
|
611
|
+
env,
|
|
612
|
+
stdio: ["pipe", "pipe", this.options.stderr === "inherit" ? "inherit" : "pipe"],
|
|
613
|
+
windowsHide: true,
|
|
614
|
+
// Own process group, so closing the transport can terminate the server's children too.
|
|
615
|
+
detached: USE_PROCESS_GROUPS
|
|
616
|
+
});
|
|
617
|
+
this.child = child;
|
|
618
|
+
const pid = child.pid;
|
|
619
|
+
if (USE_PROCESS_GROUPS && pid !== void 0) {
|
|
620
|
+
installExitHook();
|
|
621
|
+
liveProcessGroups.add(pid);
|
|
622
|
+
child.once("exit", () => liveProcessGroups.delete(pid));
|
|
623
|
+
}
|
|
624
|
+
child.stdout?.on("data", (chunk) => this.handleStdout(chunk));
|
|
625
|
+
child.stdout?.on("error", (error) => this.emitError(error));
|
|
626
|
+
child.stdin?.on("error", (error) => {
|
|
627
|
+
if (!this.closed)
|
|
628
|
+
this.emitError(error);
|
|
629
|
+
});
|
|
630
|
+
child.stderr?.on("data", (chunk) => this.handleStderr(chunk));
|
|
631
|
+
child.stderr?.on("error", (error) => this.emitError(error));
|
|
632
|
+
child.on("close", () => {
|
|
633
|
+
this.child = void 0;
|
|
634
|
+
if (this.stdoutBuffer.toString("utf8").trim()) {
|
|
635
|
+
this.emitError(new Error("MCP stdio server closed with an incomplete JSON-RPC message"));
|
|
636
|
+
}
|
|
637
|
+
this.stdoutBuffer = Buffer.alloc(0);
|
|
638
|
+
this.emitClose();
|
|
639
|
+
});
|
|
640
|
+
await new Promise((resolve, reject) => {
|
|
641
|
+
const onSpawn = () => {
|
|
642
|
+
child.off("error", onError);
|
|
643
|
+
resolve();
|
|
644
|
+
};
|
|
645
|
+
const onError = (error) => {
|
|
646
|
+
child.off("spawn", onSpawn);
|
|
647
|
+
reject(error);
|
|
648
|
+
};
|
|
649
|
+
child.once("spawn", onSpawn);
|
|
650
|
+
child.once("error", onError);
|
|
651
|
+
});
|
|
652
|
+
child.on("error", (error) => {
|
|
653
|
+
if (!this.closed)
|
|
654
|
+
this.emitError(error);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
async send(message) {
|
|
658
|
+
const stdin = this.child?.stdin;
|
|
659
|
+
if (!this.started || this.closed || !stdin?.writable)
|
|
660
|
+
throw new McpConnectionClosedError();
|
|
661
|
+
const payload = `${JSON.stringify(message)}
|
|
662
|
+
`;
|
|
663
|
+
await new Promise((resolve, reject) => {
|
|
664
|
+
stdin.write(payload, (error) => error ? reject(error) : resolve());
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
async close() {
|
|
668
|
+
if (this.closed)
|
|
669
|
+
return;
|
|
670
|
+
this.closed = true;
|
|
671
|
+
const child = this.child;
|
|
672
|
+
if (!child) {
|
|
673
|
+
this.emitClose();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
677
|
+
child.stdin?.end();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const closeTimeoutMs = this.options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
681
|
+
await new Promise((resolve) => {
|
|
682
|
+
const timers = [];
|
|
683
|
+
child.once("close", () => {
|
|
684
|
+
for (const timer of timers)
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
killProcessTree(child, "SIGTERM");
|
|
687
|
+
resolve();
|
|
688
|
+
});
|
|
689
|
+
const grace = Math.min(STDIN_CLOSE_GRACE_MS, closeTimeoutMs);
|
|
690
|
+
timers.push(setTimeout(() => killProcessTree(child, "SIGTERM"), grace));
|
|
691
|
+
timers.push(setTimeout(() => killProcessTree(child, "SIGKILL"), grace + closeTimeoutMs));
|
|
692
|
+
child.stdin?.end();
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
handleStdout(chunk) {
|
|
696
|
+
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
697
|
+
const maxMessageBytes = this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
698
|
+
while (true) {
|
|
699
|
+
const newline = this.stdoutBuffer.indexOf(10);
|
|
700
|
+
if (newline < 0) {
|
|
701
|
+
if (this.stdoutBuffer.length > maxMessageBytes) {
|
|
702
|
+
this.stdoutBuffer = Buffer.alloc(0);
|
|
703
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxMessageBytes} bytes`));
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const line = this.stdoutBuffer.subarray(0, newline);
|
|
708
|
+
this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1);
|
|
709
|
+
if (line.length > maxMessageBytes) {
|
|
710
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxMessageBytes} bytes`));
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
const text = line.toString("utf8").replace(/\r$/, "");
|
|
714
|
+
if (!text.trim())
|
|
715
|
+
continue;
|
|
716
|
+
try {
|
|
717
|
+
this.emitMessage(parseJsonRpcMessage(JSON.parse(text)));
|
|
718
|
+
} catch (error) {
|
|
719
|
+
this.emitError(error);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
handleStderr(chunk) {
|
|
724
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
725
|
+
const maxStderrBytes = this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES;
|
|
726
|
+
this.stderrBuffer = Buffer.concat([this.stderrBuffer, buffer]);
|
|
727
|
+
if (this.stderrBuffer.length > maxStderrBytes)
|
|
728
|
+
this.stderrBuffer = this.stderrBuffer.subarray(-maxStderrBytes);
|
|
729
|
+
this.options.onStderr?.(buffer.toString("utf8"));
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/streamable-http.js
|
|
734
|
+
var MAX_ERROR_BODY_BYTES = 8 * 1024;
|
|
735
|
+
var ERROR_MESSAGE_BODY_CHARS = 500;
|
|
736
|
+
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 1e3;
|
|
737
|
+
var DEFAULT_RECONNECT_MAX_DELAY_MS = 3e4;
|
|
738
|
+
var DEFAULT_RECONNECT_MAX_RETRIES = 5;
|
|
739
|
+
async function consumeSseStream(stream, options) {
|
|
740
|
+
const reader = stream.getReader();
|
|
741
|
+
const decoder = new TextDecoder();
|
|
742
|
+
let buffered = "";
|
|
743
|
+
let eventName;
|
|
744
|
+
let eventId;
|
|
745
|
+
let dataLines = [];
|
|
746
|
+
let dataBytes = 0;
|
|
747
|
+
const maxEventBytes = options.maxEventBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
748
|
+
const dispatch = () => {
|
|
749
|
+
if (dataLines.length === 0) {
|
|
750
|
+
eventName = void 0;
|
|
751
|
+
eventId = void 0;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const data = dataLines.join("\n");
|
|
755
|
+
options.onEvent({ ...eventName ? { event: eventName } : {}, data, ...eventId ? { id: eventId } : {} });
|
|
756
|
+
eventName = void 0;
|
|
757
|
+
eventId = void 0;
|
|
758
|
+
dataLines = [];
|
|
759
|
+
dataBytes = 0;
|
|
760
|
+
};
|
|
761
|
+
const processLine = (rawLine) => {
|
|
762
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
763
|
+
if (line === "") {
|
|
764
|
+
dispatch();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (line.startsWith(":"))
|
|
768
|
+
return;
|
|
769
|
+
const colon = line.indexOf(":");
|
|
770
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
771
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
772
|
+
if (value.startsWith(" "))
|
|
773
|
+
value = value.slice(1);
|
|
774
|
+
if (field === "data") {
|
|
775
|
+
dataBytes += Buffer.byteLength(value) + (dataLines.length > 0 ? 1 : 0);
|
|
776
|
+
if (dataBytes > maxEventBytes)
|
|
777
|
+
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
778
|
+
dataLines.push(value);
|
|
779
|
+
} else if (field === "event")
|
|
780
|
+
eventName = value;
|
|
781
|
+
else if (field === "id" && !value.includes("\0")) {
|
|
782
|
+
eventId = value;
|
|
783
|
+
options.onId?.(value);
|
|
784
|
+
} else if (field === "retry" && /^\d+$/.test(value))
|
|
785
|
+
options.onRetry?.(Number(value));
|
|
786
|
+
};
|
|
787
|
+
try {
|
|
788
|
+
while (true) {
|
|
789
|
+
const { done, value } = await reader.read();
|
|
790
|
+
if (done)
|
|
791
|
+
break;
|
|
792
|
+
buffered += decoder.decode(value, { stream: true });
|
|
793
|
+
let newline = buffered.indexOf("\n");
|
|
794
|
+
while (newline >= 0) {
|
|
795
|
+
processLine(buffered.slice(0, newline));
|
|
796
|
+
buffered = buffered.slice(newline + 1);
|
|
797
|
+
newline = buffered.indexOf("\n");
|
|
798
|
+
}
|
|
799
|
+
if (Buffer.byteLength(buffered) > maxEventBytes)
|
|
800
|
+
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
801
|
+
}
|
|
802
|
+
buffered += decoder.decode();
|
|
803
|
+
if (buffered)
|
|
804
|
+
processLine(buffered);
|
|
805
|
+
dispatch();
|
|
806
|
+
} finally {
|
|
807
|
+
reader.releaseLock();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
var McpHttpError = class extends Error {
|
|
811
|
+
status;
|
|
812
|
+
body;
|
|
813
|
+
constructor(status, message, body = "") {
|
|
814
|
+
super(message);
|
|
815
|
+
this.name = "McpHttpError";
|
|
816
|
+
this.status = status;
|
|
817
|
+
this.body = body;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
var McpAuthRequiredError = class extends McpHttpError {
|
|
821
|
+
wwwAuthenticate;
|
|
822
|
+
constructor(response, body = "") {
|
|
823
|
+
super(401, "MCP server requires authentication", body);
|
|
824
|
+
this.name = "McpAuthRequiredError";
|
|
825
|
+
this.wwwAuthenticate = response.headers.get("www-authenticate");
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var McpSessionExpiredError = class extends McpHttpError {
|
|
829
|
+
constructor(body = "") {
|
|
830
|
+
super(404, "MCP session expired", body);
|
|
831
|
+
this.name = "McpSessionExpiredError";
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
function contentType(response) {
|
|
835
|
+
return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
836
|
+
}
|
|
837
|
+
function needsAuthorization(response) {
|
|
838
|
+
if (response.status === 401)
|
|
839
|
+
return true;
|
|
840
|
+
if (response.status !== 403)
|
|
841
|
+
return false;
|
|
842
|
+
return /(?:^|[\s,])error="?insufficient_scope"?/i.test(response.headers.get("www-authenticate") ?? "");
|
|
843
|
+
}
|
|
844
|
+
function isTransientStatus(status) {
|
|
845
|
+
return status === 408 || status === 429 || status >= 500;
|
|
846
|
+
}
|
|
847
|
+
function discard(response) {
|
|
848
|
+
return response.body?.cancel().catch(() => {
|
|
849
|
+
}) ?? Promise.resolve();
|
|
850
|
+
}
|
|
851
|
+
function describeHttpFailure(status, body) {
|
|
852
|
+
const text = body.trim();
|
|
853
|
+
const snippet = text.length > ERROR_MESSAGE_BODY_CHARS ? `${text.slice(0, ERROR_MESSAGE_BODY_CHARS - 3)}...` : text;
|
|
854
|
+
return `MCP HTTP request failed with status ${status}${snippet ? `: ${snippet}` : ""}`;
|
|
855
|
+
}
|
|
856
|
+
var StreamableHttpTransport = class extends TransportEvents {
|
|
857
|
+
url;
|
|
858
|
+
options;
|
|
859
|
+
fetch;
|
|
860
|
+
controller = new AbortController();
|
|
861
|
+
started = false;
|
|
862
|
+
closed = false;
|
|
863
|
+
sessionIdValue;
|
|
864
|
+
protocolVersion;
|
|
865
|
+
getStreamStarted = false;
|
|
866
|
+
/** Access token of the latest request, which `close()` reuses instead of asking the auth provider. */
|
|
867
|
+
lastToken;
|
|
868
|
+
constructor(options) {
|
|
869
|
+
super();
|
|
870
|
+
this.options = Object.freeze({ ...options, headers: options.headers ? { ...options.headers } : void 0 });
|
|
871
|
+
this.url = new URL(options.url);
|
|
872
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
873
|
+
this.fetch = (input, init) => fetch(input, init);
|
|
874
|
+
}
|
|
875
|
+
get sessionId() {
|
|
876
|
+
return this.sessionIdValue;
|
|
877
|
+
}
|
|
878
|
+
async start() {
|
|
879
|
+
if (this.started)
|
|
880
|
+
throw new Error("MCP Streamable HTTP transport already started");
|
|
881
|
+
if (this.closed)
|
|
882
|
+
throw new McpConnectionClosedError();
|
|
883
|
+
this.started = true;
|
|
884
|
+
}
|
|
885
|
+
setProtocolVersion(version) {
|
|
886
|
+
this.protocolVersion = version;
|
|
887
|
+
}
|
|
888
|
+
async send(message, { signal } = {}) {
|
|
889
|
+
if (!this.started || this.closed)
|
|
890
|
+
throw new McpConnectionClosedError();
|
|
891
|
+
const response = await this.authorizedFetch("POST", {
|
|
892
|
+
headers: { accept: "application/json, text/event-stream", "content-type": "application/json" },
|
|
893
|
+
body: JSON.stringify(message)
|
|
894
|
+
}, signal);
|
|
895
|
+
await this.checkResponse(response);
|
|
896
|
+
this.captureSession(response);
|
|
897
|
+
if (!isJsonRpcRequest(message)) {
|
|
898
|
+
await discard(response);
|
|
899
|
+
if ("method" in message && message.method === "notifications/initialized")
|
|
900
|
+
this.startGetStream();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (response.status === 202 || response.status === 204) {
|
|
904
|
+
throw new McpHttpError(response.status, `MCP server accepted request ${message.method} without a response`);
|
|
905
|
+
}
|
|
906
|
+
const type = contentType(response);
|
|
907
|
+
if (type === "application/json") {
|
|
908
|
+
const body = await response.json();
|
|
909
|
+
for (const item of Array.isArray(body) ? body : [body])
|
|
910
|
+
this.emitMessage(parseJsonRpcMessage(item));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (type === "text/event-stream" && response.body) {
|
|
914
|
+
void this.consumeResponseStream(response.body, message.id);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
await discard(response);
|
|
918
|
+
throw new McpHttpError(response.status, `Unsupported MCP response content type: ${type ?? "missing"}`);
|
|
919
|
+
}
|
|
920
|
+
async close() {
|
|
921
|
+
if (this.closed)
|
|
922
|
+
return;
|
|
923
|
+
this.closed = true;
|
|
924
|
+
this.controller.abort();
|
|
925
|
+
if (this.started && this.sessionIdValue) {
|
|
926
|
+
try {
|
|
927
|
+
const response = await this.fetch(this.url, {
|
|
928
|
+
method: "DELETE",
|
|
929
|
+
headers: this.buildHeaders({}, this.lastToken),
|
|
930
|
+
signal: AbortSignal.timeout(1e3)
|
|
931
|
+
});
|
|
932
|
+
await discard(response);
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
this.emitClose();
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Fetch with auth headers. A 401 (or a 403 asking for more scope) is handed to the auth provider
|
|
940
|
+
* once, and the request is retried with whatever credentials it left behind.
|
|
941
|
+
*
|
|
942
|
+
* `signal` only stops dispatch: getting a token or the authorization recovery can take a while, and a
|
|
943
|
+
* request its sender gave up on in the meantime must not reach the server. It does not abort the shared
|
|
944
|
+
* recovery, which other requests may wait for.
|
|
945
|
+
*/
|
|
946
|
+
async authorizedFetch(method, init, signal) {
|
|
947
|
+
const onUnauthorized = this.options.authProvider?.onUnauthorized?.bind(this.options.authProvider);
|
|
948
|
+
for (let attempt = 0; ; attempt++) {
|
|
949
|
+
const { headers, token } = await this.headers(init.headers);
|
|
950
|
+
if (signal?.aborted)
|
|
951
|
+
throw new McpAbortError();
|
|
952
|
+
const response = await this.fetch(this.url, {
|
|
953
|
+
method,
|
|
954
|
+
headers,
|
|
955
|
+
body: init.body,
|
|
956
|
+
signal: this.controller.signal
|
|
957
|
+
});
|
|
958
|
+
if (attempt > 0 || !onUnauthorized || !needsAuthorization(response))
|
|
959
|
+
return response;
|
|
960
|
+
try {
|
|
961
|
+
await onUnauthorized({ response, serverUrl: this.url, fetch: this.fetch, token });
|
|
962
|
+
} finally {
|
|
963
|
+
await discard(response);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async headers(extra = {}) {
|
|
968
|
+
const token = await this.options.authProvider?.token();
|
|
969
|
+
this.lastToken = token;
|
|
970
|
+
return { headers: this.buildHeaders(extra, token), ...token ? { token } : {} };
|
|
971
|
+
}
|
|
972
|
+
buildHeaders(extra, token) {
|
|
973
|
+
const headers = new Headers(this.options.headers);
|
|
974
|
+
for (const [name, value] of Object.entries(extra))
|
|
975
|
+
headers.set(name, value);
|
|
976
|
+
if (this.sessionIdValue)
|
|
977
|
+
headers.set("Mcp-Session-Id", this.sessionIdValue);
|
|
978
|
+
if (this.protocolVersion)
|
|
979
|
+
headers.set("MCP-Protocol-Version", this.protocolVersion);
|
|
980
|
+
if (token)
|
|
981
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
982
|
+
return headers;
|
|
983
|
+
}
|
|
984
|
+
captureSession(response) {
|
|
985
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
986
|
+
if (sessionId)
|
|
987
|
+
this.sessionIdValue = sessionId;
|
|
988
|
+
}
|
|
989
|
+
async checkResponse(response) {
|
|
990
|
+
if (response.ok)
|
|
991
|
+
return;
|
|
992
|
+
const body = (await response.text().catch(() => "")).slice(0, MAX_ERROR_BODY_BYTES);
|
|
993
|
+
if (response.status === 401)
|
|
994
|
+
throw new McpAuthRequiredError(response, body);
|
|
995
|
+
if (response.status === 404 && this.sessionIdValue)
|
|
996
|
+
throw new McpSessionExpiredError(body);
|
|
997
|
+
throw new McpHttpError(response.status, describeHttpFailure(response.status, body), body);
|
|
998
|
+
}
|
|
999
|
+
async consumeSse(stream, cursor, onMessage) {
|
|
1000
|
+
await consumeSseStream(stream, {
|
|
1001
|
+
maxEventBytes: this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES,
|
|
1002
|
+
onId: (id) => {
|
|
1003
|
+
cursor.lastEventId = id;
|
|
1004
|
+
},
|
|
1005
|
+
onRetry: (delayMs) => {
|
|
1006
|
+
cursor.retryMs = delayMs;
|
|
1007
|
+
},
|
|
1008
|
+
onEvent: (event) => {
|
|
1009
|
+
cursor.received = true;
|
|
1010
|
+
if (!event.data.trim() || event.event !== void 0 && event.event !== "message")
|
|
1011
|
+
return;
|
|
1012
|
+
let message;
|
|
1013
|
+
try {
|
|
1014
|
+
message = parseJsonRpcMessage(JSON.parse(event.data));
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
this.emitError(error);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
onMessage?.(message);
|
|
1020
|
+
this.emitMessage(message);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Read the SSE stream answering one request. When the stream ends or breaks before the response
|
|
1026
|
+
* arrives and the server assigned event IDs, resume it with GET and `Last-Event-ID`, as the
|
|
1027
|
+
* server may close response streams at will. Otherwise only this request fails.
|
|
1028
|
+
*/
|
|
1029
|
+
async consumeResponseStream(body, requestId) {
|
|
1030
|
+
const cursor = { lastEventId: void 0, retryMs: void 0, received: false };
|
|
1031
|
+
let answered = false;
|
|
1032
|
+
const onMessage = (message) => {
|
|
1033
|
+
if (isJsonRpcResponse(message) && message.id === requestId)
|
|
1034
|
+
answered = true;
|
|
1035
|
+
};
|
|
1036
|
+
let stream = body;
|
|
1037
|
+
let failure;
|
|
1038
|
+
for (let attempt = 0; ; ) {
|
|
1039
|
+
if (stream) {
|
|
1040
|
+
try {
|
|
1041
|
+
await this.consumeSse(stream, cursor, onMessage);
|
|
1042
|
+
failure = void 0;
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
failure = error;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (answered || this.closed)
|
|
1048
|
+
return;
|
|
1049
|
+
if (failure !== void 0 && !this.isRetryable(failure))
|
|
1050
|
+
break;
|
|
1051
|
+
if (cursor.lastEventId === void 0 || attempt >= this.maxRetries())
|
|
1052
|
+
break;
|
|
1053
|
+
if (cursor.received)
|
|
1054
|
+
attempt = 0;
|
|
1055
|
+
cursor.received = false;
|
|
1056
|
+
if (!await this.sleep(this.reconnectDelay(attempt++, cursor.retryMs)))
|
|
1057
|
+
return;
|
|
1058
|
+
try {
|
|
1059
|
+
stream = await this.openSseStream(cursor.lastEventId);
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
failure = error;
|
|
1062
|
+
if (!this.isRetryable(error))
|
|
1063
|
+
break;
|
|
1064
|
+
stream = void 0;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (this.closed)
|
|
1068
|
+
return;
|
|
1069
|
+
const reason = failure === void 0 ? "stream ended without a response" : toError(failure).message;
|
|
1070
|
+
this.emitMessage({
|
|
1071
|
+
jsonrpc: "2.0",
|
|
1072
|
+
id: requestId,
|
|
1073
|
+
error: { code: JSON_RPC_ERROR_CODES.internalError, message: `MCP response stream failed: ${reason}` }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
startGetStream() {
|
|
1077
|
+
if (this.options.openGetStream === false || this.getStreamStarted || this.closed)
|
|
1078
|
+
return;
|
|
1079
|
+
this.getStreamStarted = true;
|
|
1080
|
+
void this.runGetStream();
|
|
1081
|
+
}
|
|
1082
|
+
/** Keep the server-to-client stream open, reconnecting with backoff when it drops. */
|
|
1083
|
+
async runGetStream() {
|
|
1084
|
+
const cursor = { lastEventId: void 0, retryMs: void 0, received: false };
|
|
1085
|
+
for (let attempt = 0; !this.closed; ) {
|
|
1086
|
+
try {
|
|
1087
|
+
const stream = await this.openSseStream(cursor.lastEventId);
|
|
1088
|
+
if (!stream)
|
|
1089
|
+
return;
|
|
1090
|
+
const openedAt = Date.now();
|
|
1091
|
+
await this.consumeSse(stream, cursor);
|
|
1092
|
+
if (cursor.received || Date.now() - openedAt > this.maxDelay())
|
|
1093
|
+
attempt = 0;
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (this.closed)
|
|
1096
|
+
return;
|
|
1097
|
+
if (!this.isRetryable(error)) {
|
|
1098
|
+
this.emitError(error);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
cursor.received = false;
|
|
1103
|
+
if (attempt >= this.maxRetries()) {
|
|
1104
|
+
this.emitError(new Error("MCP server-to-client stream dropped and could not be reopened"));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (!await this.sleep(this.reconnectDelay(attempt++, cursor.retryMs)))
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/** Open a GET SSE stream. Resolves to undefined when the server answers 405 (no GET stream). */
|
|
1112
|
+
async openSseStream(lastEventId) {
|
|
1113
|
+
const response = await this.authorizedFetch("GET", {
|
|
1114
|
+
headers: {
|
|
1115
|
+
accept: "text/event-stream",
|
|
1116
|
+
...lastEventId === void 0 ? {} : { "last-event-id": lastEventId }
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
if (response.status === 405) {
|
|
1120
|
+
await discard(response);
|
|
1121
|
+
return void 0;
|
|
1122
|
+
}
|
|
1123
|
+
await this.checkResponse(response);
|
|
1124
|
+
this.captureSession(response);
|
|
1125
|
+
const type = contentType(response);
|
|
1126
|
+
if (type !== "text/event-stream" || !response.body) {
|
|
1127
|
+
await discard(response);
|
|
1128
|
+
throw new McpHttpError(response.status, `Unsupported MCP GET response content type: ${type ?? "missing"}`);
|
|
1129
|
+
}
|
|
1130
|
+
return response.body;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Network failures and transient statuses are retried; auth, session, and protocol errors are not.
|
|
1134
|
+
* `fetch` reports network failures, including a connection dropped mid-body, as `TypeError`.
|
|
1135
|
+
*/
|
|
1136
|
+
isRetryable(error) {
|
|
1137
|
+
if (error instanceof McpHttpError)
|
|
1138
|
+
return isTransientStatus(error.status);
|
|
1139
|
+
if (error instanceof TypeError)
|
|
1140
|
+
return true;
|
|
1141
|
+
const code = error?.code;
|
|
1142
|
+
return typeof code === "string" && (code.startsWith("E") || code.startsWith("UND_ERR"));
|
|
1143
|
+
}
|
|
1144
|
+
reconnectDelay(attempt, serverDelayMs) {
|
|
1145
|
+
if (serverDelayMs !== void 0)
|
|
1146
|
+
return serverDelayMs;
|
|
1147
|
+
const initial = this.options.reconnect?.initialDelayMs ?? DEFAULT_RECONNECT_INITIAL_DELAY_MS;
|
|
1148
|
+
return Math.min(initial * 2 ** attempt, this.maxDelay());
|
|
1149
|
+
}
|
|
1150
|
+
maxDelay() {
|
|
1151
|
+
return this.options.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
1152
|
+
}
|
|
1153
|
+
maxRetries() {
|
|
1154
|
+
return this.options.reconnect?.maxRetries ?? DEFAULT_RECONNECT_MAX_RETRIES;
|
|
1155
|
+
}
|
|
1156
|
+
/** Resolves false when the transport closed while waiting. */
|
|
1157
|
+
sleep(ms) {
|
|
1158
|
+
const signal = this.controller.signal;
|
|
1159
|
+
if (signal.aborted)
|
|
1160
|
+
return Promise.resolve(false);
|
|
1161
|
+
return new Promise((resolve) => {
|
|
1162
|
+
const onAbort = () => {
|
|
1163
|
+
clearTimeout(timer);
|
|
1164
|
+
resolve(false);
|
|
1165
|
+
};
|
|
1166
|
+
const timer = setTimeout(() => {
|
|
1167
|
+
signal.removeEventListener("abort", onAbort);
|
|
1168
|
+
resolve(true);
|
|
1169
|
+
}, ms);
|
|
1170
|
+
timer.unref?.();
|
|
1171
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
// src/client/pi-connection.ts
|
|
1177
|
+
async function createPiConnection(options) {
|
|
1178
|
+
const clientOptions = {
|
|
1179
|
+
name: "mcpcat",
|
|
1180
|
+
version: options.version ?? "0.0.0"
|
|
1181
|
+
};
|
|
1182
|
+
const client = new McpClient(clientOptions);
|
|
1183
|
+
let transport;
|
|
1184
|
+
if (options.url) {
|
|
1185
|
+
transport = new StreamableHttpTransport({
|
|
1186
|
+
url: options.url,
|
|
1187
|
+
headers: options.headers
|
|
1188
|
+
});
|
|
1189
|
+
} else if (options.command) {
|
|
1190
|
+
transport = new StdioTransport({
|
|
1191
|
+
command: options.command,
|
|
1192
|
+
args: options.args,
|
|
1193
|
+
env: options.env,
|
|
1194
|
+
cwd: options.cwd,
|
|
1195
|
+
inheritEnv: true,
|
|
1196
|
+
stderr: "pipe"
|
|
1197
|
+
});
|
|
1198
|
+
} else {
|
|
1199
|
+
throw new Error("PiConnection requires either a url or a command");
|
|
1200
|
+
}
|
|
1201
|
+
const initResult = await client.connect(transport);
|
|
1202
|
+
return {
|
|
1203
|
+
async info() {
|
|
1204
|
+
return {
|
|
1205
|
+
name: initResult.serverInfo?.name,
|
|
1206
|
+
version: initResult.serverInfo?.version,
|
|
1207
|
+
title: initResult.serverInfo?.title,
|
|
1208
|
+
protocolVersion: initResult.protocolVersion,
|
|
1209
|
+
capabilities: initResult.capabilities,
|
|
1210
|
+
instructions: initResult.instructions
|
|
1211
|
+
};
|
|
1212
|
+
},
|
|
1213
|
+
async listTools() {
|
|
1214
|
+
return client.listTools();
|
|
1215
|
+
},
|
|
1216
|
+
async callTool(name, args) {
|
|
1217
|
+
return client.callTool(name, args);
|
|
1218
|
+
},
|
|
1219
|
+
async close() {
|
|
1220
|
+
await client.close();
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/client/stateless-connection.ts
|
|
1226
|
+
var STATELESS_PROTOCOL_VERSION = "2026-07-28";
|
|
1227
|
+
var StatelessRpcError = class extends Error {
|
|
1228
|
+
code;
|
|
1229
|
+
data;
|
|
1230
|
+
constructor(code, message, data) {
|
|
1231
|
+
super(`MCP RPC error ${code}: ${message}`);
|
|
1232
|
+
this.name = "StatelessRpcError";
|
|
1233
|
+
this.code = code;
|
|
1234
|
+
this.data = data;
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
var StatelessHttpError = class extends Error {
|
|
1238
|
+
status;
|
|
1239
|
+
constructor(status, statusText, method, snippet) {
|
|
1240
|
+
super(`MCP HTTP ${status} ${statusText} for ${method}: ${snippet || "(empty body)"}`);
|
|
1241
|
+
this.name = "StatelessHttpError";
|
|
1242
|
+
this.status = status;
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
var InputRequiredError = class extends Error {
|
|
1246
|
+
requestJson;
|
|
1247
|
+
constructor(requestJson) {
|
|
1248
|
+
super("Server requires input (resultType: input_required)");
|
|
1249
|
+
this.name = "InputRequiredError";
|
|
1250
|
+
this.requestJson = requestJson;
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
function selectSseFrame(body, targetId) {
|
|
1254
|
+
const text = new TextDecoder().decode(body);
|
|
1255
|
+
const blocks = text.replace(/\r\n/g, "\n").split(/\n\n/);
|
|
1256
|
+
for (const block of blocks) {
|
|
1257
|
+
if (!block.trim()) continue;
|
|
1258
|
+
const dataLines = [];
|
|
1259
|
+
for (const line of block.split("\n")) {
|
|
1260
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
1261
|
+
}
|
|
1262
|
+
if (dataLines.length === 0) continue;
|
|
1263
|
+
try {
|
|
1264
|
+
const parsed = JSON.parse(dataLines.join("\n"));
|
|
1265
|
+
if (parsed.id === targetId) return parsed;
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
throw new Error(`SSE stream ended without response for id ${targetId}`);
|
|
1270
|
+
}
|
|
1271
|
+
function mcpNameForMethod(method, params) {
|
|
1272
|
+
if (method === "tools/call") return params.name;
|
|
1273
|
+
if (method === "prompts/get") return params.name;
|
|
1274
|
+
if (method === "resources/read") return params.uri;
|
|
1275
|
+
return void 0;
|
|
1276
|
+
}
|
|
1277
|
+
async function createStatelessConnection(options) {
|
|
1278
|
+
const {
|
|
1279
|
+
url,
|
|
1280
|
+
headers: extraHeaders,
|
|
1281
|
+
fetchImpl,
|
|
1282
|
+
timeoutMs = 6e4,
|
|
1283
|
+
clientInfo = { name: "mcpcat", version: "0.0.0" }
|
|
1284
|
+
} = options;
|
|
1285
|
+
let nextId = 1;
|
|
1286
|
+
let negotiatedVersion;
|
|
1287
|
+
async function request(method, params) {
|
|
1288
|
+
const id = nextId++;
|
|
1289
|
+
const headers = {
|
|
1290
|
+
"Content-Type": "application/json",
|
|
1291
|
+
Accept: "application/json, text/event-stream",
|
|
1292
|
+
"Mcp-Method": method,
|
|
1293
|
+
...extraHeaders
|
|
1294
|
+
};
|
|
1295
|
+
const mcpName = mcpNameForMethod(method, params);
|
|
1296
|
+
if (mcpName) {
|
|
1297
|
+
headers["Mcp-Name"] = mcpName;
|
|
1298
|
+
}
|
|
1299
|
+
if (negotiatedVersion) {
|
|
1300
|
+
headers["Mcp-Protocol-Version"] = negotiatedVersion;
|
|
1301
|
+
}
|
|
1302
|
+
const meta = {
|
|
1303
|
+
protocolVersion: negotiatedVersion ?? STATELESS_PROTOCOL_VERSION,
|
|
1304
|
+
clientInfo
|
|
1305
|
+
};
|
|
1306
|
+
const body = JSON.stringify({
|
|
1307
|
+
jsonrpc: "2.0",
|
|
1308
|
+
id,
|
|
1309
|
+
method,
|
|
1310
|
+
params: { ...params, _meta: meta }
|
|
1311
|
+
});
|
|
1312
|
+
const controller = new AbortController();
|
|
1313
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1314
|
+
let res;
|
|
1315
|
+
try {
|
|
1316
|
+
res = await fetchImpl(url, {
|
|
1317
|
+
method: "POST",
|
|
1318
|
+
headers,
|
|
1319
|
+
body,
|
|
1320
|
+
signal: controller.signal
|
|
1321
|
+
});
|
|
1322
|
+
} finally {
|
|
1323
|
+
clearTimeout(timer);
|
|
1324
|
+
}
|
|
1325
|
+
if (res.status === 401) {
|
|
1326
|
+
throw new StatelessHttpError(401, "Unauthorized", method, "");
|
|
1327
|
+
}
|
|
1328
|
+
if (res.status >= 400) {
|
|
1329
|
+
const text = await res.text();
|
|
1330
|
+
let rpcError;
|
|
1331
|
+
try {
|
|
1332
|
+
const parsed = JSON.parse(text);
|
|
1333
|
+
if (parsed.error && typeof parsed.error.code === "number") {
|
|
1334
|
+
rpcError = parsed.error;
|
|
1335
|
+
}
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
if (rpcError) {
|
|
1339
|
+
throw new StatelessRpcError(rpcError.code, rpcError.message, rpcError.data);
|
|
1340
|
+
}
|
|
1341
|
+
throw new StatelessHttpError(res.status, res.statusText, method, text.slice(0, 512));
|
|
1342
|
+
}
|
|
1343
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
1344
|
+
let frame;
|
|
1345
|
+
if (ct.toLowerCase().includes("text/event-stream")) {
|
|
1346
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1347
|
+
frame = selectSseFrame(bytes, id);
|
|
1348
|
+
} else {
|
|
1349
|
+
frame = await res.json();
|
|
1350
|
+
}
|
|
1351
|
+
if (frame.error) {
|
|
1352
|
+
throw new StatelessRpcError(frame.error.code, frame.error.message, frame.error.data);
|
|
1353
|
+
}
|
|
1354
|
+
return frame.result;
|
|
1355
|
+
}
|
|
1356
|
+
const discoverResult = await request("server/discover", {
|
|
1357
|
+
protocolVersion: STATELESS_PROTOCOL_VERSION
|
|
1358
|
+
});
|
|
1359
|
+
const supported = Array.isArray(discoverResult?.supportedVersions) ? discoverResult.supportedVersions.filter((v) => typeof v === "string") : [];
|
|
1360
|
+
if (!supported.includes(STATELESS_PROTOCOL_VERSION)) {
|
|
1361
|
+
throw new Error(
|
|
1362
|
+
`Server does not support ${STATELESS_PROTOCOL_VERSION} (advertised: ${supported.join(", ") || "none"})`
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
negotiatedVersion = STATELESS_PROTOCOL_VERSION;
|
|
1366
|
+
let serverInfoCache;
|
|
1367
|
+
return {
|
|
1368
|
+
async info() {
|
|
1369
|
+
if (serverInfoCache) return serverInfoCache;
|
|
1370
|
+
const result = discoverResult;
|
|
1371
|
+
serverInfoCache = {
|
|
1372
|
+
name: result?.serverInfo?.name,
|
|
1373
|
+
version: result?.serverInfo?.version,
|
|
1374
|
+
title: result?.serverInfo?.title,
|
|
1375
|
+
protocolVersion: STATELESS_PROTOCOL_VERSION,
|
|
1376
|
+
capabilities: result?.capabilities ?? {},
|
|
1377
|
+
instructions: result?.instructions
|
|
1378
|
+
};
|
|
1379
|
+
return serverInfoCache;
|
|
1380
|
+
},
|
|
1381
|
+
async listTools() {
|
|
1382
|
+
const result = await request("tools/list", {});
|
|
1383
|
+
return result?.tools ?? [];
|
|
1384
|
+
},
|
|
1385
|
+
async callTool(name, args) {
|
|
1386
|
+
const result = await request("tools/call", {
|
|
1387
|
+
name,
|
|
1388
|
+
arguments: args ?? {}
|
|
1389
|
+
});
|
|
1390
|
+
const resultType = result.resultType ?? "complete";
|
|
1391
|
+
if (resultType === "input_required") {
|
|
1392
|
+
throw new InputRequiredError(
|
|
1393
|
+
JSON.stringify({ name, arguments: args ?? {}, result }, null, 2)
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
return result;
|
|
1397
|
+
},
|
|
1398
|
+
async close() {
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/client/connect.ts
|
|
1404
|
+
async function connect(platform, options) {
|
|
1405
|
+
if (options.command) {
|
|
1406
|
+
return createPiConnection({
|
|
1407
|
+
command: options.command,
|
|
1408
|
+
args: options.args,
|
|
1409
|
+
env: options.env,
|
|
1410
|
+
cwd: options.cwd,
|
|
1411
|
+
version: options.version
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
if (!options.url) {
|
|
1415
|
+
throw new Error("connect requires either a url or a command");
|
|
1416
|
+
}
|
|
1417
|
+
if (options.protocolVersion && !options.protocolVersion.startsWith("2026")) {
|
|
1418
|
+
return createPiConnection({
|
|
1419
|
+
url: options.url,
|
|
1420
|
+
headers: options.headers,
|
|
1421
|
+
version: options.version
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
try {
|
|
1425
|
+
return await createStatelessConnection({
|
|
1426
|
+
url: options.url,
|
|
1427
|
+
headers: options.headers,
|
|
1428
|
+
fetchImpl: platform.fetch,
|
|
1429
|
+
clientInfo: { name: "mcpcat", version: options.version ?? "0.0.0" }
|
|
1430
|
+
});
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
|
|
1433
|
+
if (options.protocolVersion?.startsWith("2026")) {
|
|
1434
|
+
throw err;
|
|
1435
|
+
}
|
|
1436
|
+
if (isMethodNotFound) {
|
|
1437
|
+
return createPiConnection({
|
|
1438
|
+
url: options.url,
|
|
1439
|
+
headers: options.headers,
|
|
1440
|
+
version: options.version
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
throw err;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/client/targets.ts
|
|
1448
|
+
function splitShellStyle(input) {
|
|
1449
|
+
const result = [];
|
|
1450
|
+
let current = "";
|
|
1451
|
+
let inSingle = false;
|
|
1452
|
+
let inDouble = false;
|
|
1453
|
+
for (let i = 0; i < input.length; i++) {
|
|
1454
|
+
const c = input[i];
|
|
1455
|
+
if (inSingle) {
|
|
1456
|
+
if (c === "'") {
|
|
1457
|
+
inSingle = false;
|
|
1458
|
+
} else {
|
|
1459
|
+
current += c;
|
|
1460
|
+
}
|
|
1461
|
+
} else if (inDouble) {
|
|
1462
|
+
if (c === '"') {
|
|
1463
|
+
inDouble = false;
|
|
1464
|
+
} else if (c === "\\" && i + 1 < input.length) {
|
|
1465
|
+
current += input[++i];
|
|
1466
|
+
} else {
|
|
1467
|
+
current += c;
|
|
1468
|
+
}
|
|
1469
|
+
} else if (c === "'") {
|
|
1470
|
+
inSingle = true;
|
|
1471
|
+
} else if (c === '"') {
|
|
1472
|
+
inDouble = true;
|
|
1473
|
+
} else if (c === " " || c === " ") {
|
|
1474
|
+
if (current.length > 0) {
|
|
1475
|
+
result.push(current);
|
|
1476
|
+
current = "";
|
|
1477
|
+
}
|
|
1478
|
+
} else if (c === "\\" && i + 1 < input.length) {
|
|
1479
|
+
current += input[++i];
|
|
1480
|
+
} else {
|
|
1481
|
+
current += c;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (current.length > 0) result.push(current);
|
|
1485
|
+
return result;
|
|
1486
|
+
}
|
|
1487
|
+
function normalizeUrl(raw) {
|
|
1488
|
+
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
|
|
1489
|
+
if (raw.startsWith("localhost") || raw.startsWith("127.0.0.1") || raw.startsWith("[::1]")) {
|
|
1490
|
+
return `http://${raw}`;
|
|
1491
|
+
}
|
|
1492
|
+
return `https://${raw}`;
|
|
1493
|
+
}
|
|
1494
|
+
function looksLikeUrl(value) {
|
|
1495
|
+
if (value.startsWith("http://") || value.startsWith("https://")) return true;
|
|
1496
|
+
if (value.includes(".") && !value.includes(" ")) return true;
|
|
1497
|
+
if (value.startsWith("localhost") || value.startsWith("127.0.0.1") || value.startsWith("[::1]"))
|
|
1498
|
+
return true;
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
async function resolveTarget(target, trailingArgs, config) {
|
|
1502
|
+
if (trailingArgs && trailingArgs.length > 0) {
|
|
1503
|
+
return { command: trailingArgs[0], args: trailingArgs.slice(1) };
|
|
1504
|
+
}
|
|
1505
|
+
if (!target) {
|
|
1506
|
+
throw new Error("no target given");
|
|
1507
|
+
}
|
|
1508
|
+
if (target.startsWith("stdio:")) {
|
|
1509
|
+
const rest = target.slice(6);
|
|
1510
|
+
const parts = splitShellStyle(rest);
|
|
1511
|
+
if (parts.length === 0) throw new Error("stdio: target requires a command");
|
|
1512
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
1513
|
+
}
|
|
1514
|
+
if (looksLikeUrl(target)) {
|
|
1515
|
+
return { url: normalizeUrl(target) };
|
|
1516
|
+
}
|
|
1517
|
+
const entry = await config.get(target);
|
|
1518
|
+
if (entry) {
|
|
1519
|
+
return configToTarget(entry);
|
|
1520
|
+
}
|
|
1521
|
+
throw new Error(`unknown target '${target}': not a URL and not found in mcp.json`);
|
|
1522
|
+
}
|
|
1523
|
+
function configToTarget(entry) {
|
|
1524
|
+
if (entry.url) {
|
|
1525
|
+
return {
|
|
1526
|
+
url: entry.url,
|
|
1527
|
+
headers: entry.headers
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
if (entry.command) {
|
|
1531
|
+
return {
|
|
1532
|
+
command: entry.command,
|
|
1533
|
+
args: entry.args,
|
|
1534
|
+
env: entry.env,
|
|
1535
|
+
cwd: entry.cwd
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
throw new Error("config entry has neither url nor command");
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/config/source.ts
|
|
1542
|
+
function createConfigSource(platform) {
|
|
1543
|
+
const home = platform.env.HOME ?? platform.env.USERPROFILE ?? "";
|
|
1544
|
+
const piDir = platform.env.PI_CODING_AGENT_DIR ?? `${home}/.pi/agent`;
|
|
1545
|
+
const configPath = `${piDir}/mcp.json`;
|
|
1546
|
+
return {
|
|
1547
|
+
async get(name) {
|
|
1548
|
+
try {
|
|
1549
|
+
const exists = await platform.fs.exists(configPath);
|
|
1550
|
+
if (!exists) return void 0;
|
|
1551
|
+
const data = await platform.fs.readFile(configPath);
|
|
1552
|
+
const text = new TextDecoder().decode(data);
|
|
1553
|
+
const parsed = JSON.parse(text);
|
|
1554
|
+
if (!parsed.mcpServers) return void 0;
|
|
1555
|
+
const normalizedName = name.replace(/[-_]/g, "").toLowerCase();
|
|
1556
|
+
for (const [key, value] of Object.entries(parsed.mcpServers)) {
|
|
1557
|
+
if (key.replace(/[-_]/g, "").toLowerCase() === normalizedName) {
|
|
1558
|
+
return value;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return void 0;
|
|
1562
|
+
} catch {
|
|
1563
|
+
return void 0;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/cli/main.ts
|
|
1570
|
+
var VERBS = [
|
|
1571
|
+
"info",
|
|
1572
|
+
"tools",
|
|
1573
|
+
"ls",
|
|
1574
|
+
"call",
|
|
1575
|
+
"describe",
|
|
1576
|
+
"resources",
|
|
1577
|
+
"read",
|
|
1578
|
+
"prompts",
|
|
1579
|
+
"prompt",
|
|
1580
|
+
"grep",
|
|
1581
|
+
"complete",
|
|
1582
|
+
"task",
|
|
1583
|
+
"add",
|
|
1584
|
+
"rm",
|
|
1585
|
+
"list",
|
|
1586
|
+
"enable",
|
|
1587
|
+
"disable",
|
|
1588
|
+
"exposure",
|
|
1589
|
+
"import",
|
|
1590
|
+
"login",
|
|
1591
|
+
"logout",
|
|
1592
|
+
"serve",
|
|
1593
|
+
"code",
|
|
1594
|
+
"discover",
|
|
1595
|
+
"inbox",
|
|
1596
|
+
"help"
|
|
1597
|
+
];
|
|
1598
|
+
var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
1599
|
+
"resources",
|
|
1600
|
+
"read",
|
|
1601
|
+
"prompts",
|
|
1602
|
+
"prompt",
|
|
1603
|
+
"grep",
|
|
1604
|
+
"complete",
|
|
1605
|
+
"task",
|
|
1606
|
+
"add",
|
|
1607
|
+
"rm",
|
|
1608
|
+
"list",
|
|
1609
|
+
"enable",
|
|
1610
|
+
"disable",
|
|
1611
|
+
"exposure",
|
|
1612
|
+
"import",
|
|
1613
|
+
"login",
|
|
1614
|
+
"logout",
|
|
1615
|
+
"serve",
|
|
1616
|
+
"code",
|
|
1617
|
+
"discover",
|
|
1618
|
+
"inbox"
|
|
1619
|
+
]);
|
|
1620
|
+
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe"]);
|
|
1621
|
+
function helpText(version) {
|
|
1622
|
+
return `mcpcat ${version} \u2013 a curl for the Model Context Protocol
|
|
1623
|
+
|
|
1624
|
+
Usage: mcpcat [options] <target> [<tool> [args\u2026]]
|
|
1625
|
+
mcpcat [options] <verb> \u2026
|
|
1626
|
+
|
|
1627
|
+
Client verbs:
|
|
1628
|
+
info <target> Server identity and capabilities
|
|
1629
|
+
tools <target> List tools
|
|
1630
|
+
call <target> <tool> Call a tool
|
|
1631
|
+
describe <target> [tool] Tool schema and help
|
|
1632
|
+
|
|
1633
|
+
Configuration verbs:
|
|
1634
|
+
add <name> <url> Add a server to mcp.json (not implemented yet)
|
|
1635
|
+
rm <name> Remove a server (not implemented yet)
|
|
1636
|
+
list List configured servers (not implemented yet)
|
|
1637
|
+
enable|disable <name> Toggle a server (not implemented yet)
|
|
1638
|
+
exposure <name> <mode> Set exposure mode (not implemented yet)
|
|
1639
|
+
import <file> Import from other configs (not implemented yet)
|
|
1640
|
+
login <target> OAuth sign-in (not implemented yet)
|
|
1641
|
+
logout <target> Remove tokens (not implemented yet)
|
|
1642
|
+
|
|
1643
|
+
Server and code verbs:
|
|
1644
|
+
serve [--stdio|--http] \u2026 Expose commands as MCP server (not implemented yet)
|
|
1645
|
+
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
1646
|
+
discover <url> Find MCP servers (not implemented yet)
|
|
1647
|
+
inbox Discovery inbox (not implemented yet)
|
|
1648
|
+
help [verb] Show help
|
|
1649
|
+
|
|
1650
|
+
Options:
|
|
1651
|
+
--json JSON output
|
|
1652
|
+
-H, --header 'N: V' Extra header
|
|
1653
|
+
-d <json> Tool arguments as JSON (or @file, @- for stdin)
|
|
1654
|
+
-m, --timeout <s> Request timeout (default 60)
|
|
1655
|
+
--protocol-version <v> Pin protocol version
|
|
1656
|
+
--schema Print raw JSON Schema (describe)
|
|
1657
|
+
-v, --verbose Trace on stderr
|
|
1658
|
+
-s, --silent Suppress progress
|
|
1659
|
+
-o, --output <path> Write result to file
|
|
1660
|
+
-V, --version Print version
|
|
1661
|
+
-h, --help Show this help
|
|
1662
|
+
`;
|
|
1663
|
+
}
|
|
1664
|
+
function parseFlags(args) {
|
|
1665
|
+
const headers = {};
|
|
1666
|
+
let dataArg;
|
|
1667
|
+
let protocolVersion;
|
|
1668
|
+
let showSchema = false;
|
|
1669
|
+
const positional = [];
|
|
1670
|
+
let trailingCommand;
|
|
1671
|
+
for (let i = 0; i < args.length; i++) {
|
|
1672
|
+
const arg = args[i];
|
|
1673
|
+
if (arg === "--") {
|
|
1674
|
+
trailingCommand = args.slice(i + 1);
|
|
1675
|
+
break;
|
|
1676
|
+
}
|
|
1677
|
+
if ((arg === "-H" || arg === "--header") && args[i + 1]) {
|
|
1678
|
+
const val = args[++i];
|
|
1679
|
+
const colonIdx = val.indexOf(":");
|
|
1680
|
+
if (colonIdx > 0) {
|
|
1681
|
+
headers[val.slice(0, colonIdx).trim()] = val.slice(colonIdx + 1).trim();
|
|
1682
|
+
}
|
|
1683
|
+
} else if (arg === "-d" && args[i + 1]) {
|
|
1684
|
+
dataArg = args[++i];
|
|
1685
|
+
} else if (arg === "--protocol-version" && args[i + 1]) {
|
|
1686
|
+
protocolVersion = args[++i];
|
|
1687
|
+
} else if (arg === "--schema") {
|
|
1688
|
+
showSchema = true;
|
|
1689
|
+
} else if (!arg.startsWith("-")) {
|
|
1690
|
+
positional.push(arg);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return { headers, dataArg, protocolVersion, showSchema, positional, trailingCommand };
|
|
1694
|
+
}
|
|
1695
|
+
function resolveVerb(positional, trailingCommand) {
|
|
1696
|
+
let verb;
|
|
1697
|
+
let target;
|
|
1698
|
+
let toolName;
|
|
1699
|
+
if (positional.length >= 1 && VERBS.includes(positional[0])) {
|
|
1700
|
+
verb = positional[0];
|
|
1701
|
+
if (verb === "ls") verb = "tools";
|
|
1702
|
+
target = positional[1];
|
|
1703
|
+
toolName = positional[2];
|
|
1704
|
+
} else if (positional.length >= 2) {
|
|
1705
|
+
target = positional[0];
|
|
1706
|
+
toolName = positional[1];
|
|
1707
|
+
verb = "call";
|
|
1708
|
+
} else if (positional.length === 1) {
|
|
1709
|
+
target = positional[0];
|
|
1710
|
+
verb = "info";
|
|
1711
|
+
}
|
|
1712
|
+
if (!target && trailingCommand && trailingCommand.length > 0) {
|
|
1713
|
+
target = "__trailing__";
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
verb,
|
|
1717
|
+
target,
|
|
1718
|
+
toolName,
|
|
1719
|
+
dataArg: void 0,
|
|
1720
|
+
headers: {},
|
|
1721
|
+
protocolVersion: void 0,
|
|
1722
|
+
showSchema: false,
|
|
1723
|
+
trailingCommand
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function parseArgs(args) {
|
|
1727
|
+
const flags = parseFlags(args);
|
|
1728
|
+
const resolved = resolveVerb(flags.positional, flags.trailingCommand);
|
|
1729
|
+
return {
|
|
1730
|
+
...resolved,
|
|
1731
|
+
headers: flags.headers,
|
|
1732
|
+
dataArg: flags.dataArg,
|
|
1733
|
+
protocolVersion: flags.protocolVersion,
|
|
1734
|
+
showSchema: flags.showSchema
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function formatToolLine(tool) {
|
|
1738
|
+
const desc = tool.description ? ` ${tool.description}` : "";
|
|
1739
|
+
return `${tool.name}${desc}`;
|
|
1740
|
+
}
|
|
1741
|
+
function formatToolSchema(tool) {
|
|
1742
|
+
const lines = [];
|
|
1743
|
+
lines.push(`${tool.name}`);
|
|
1744
|
+
if (tool.description) lines.push(` ${tool.description}`);
|
|
1745
|
+
if (tool.inputSchema) {
|
|
1746
|
+
lines.push(" Input schema:");
|
|
1747
|
+
lines.push(` ${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`);
|
|
1748
|
+
}
|
|
1749
|
+
const annotations = tool.annotations;
|
|
1750
|
+
if (annotations) {
|
|
1751
|
+
lines.push(" Annotations:");
|
|
1752
|
+
for (const [k, v] of Object.entries(annotations)) {
|
|
1753
|
+
lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return lines.join("\n");
|
|
1757
|
+
}
|
|
1758
|
+
function renderCallResult(result) {
|
|
1759
|
+
const parts = [];
|
|
1760
|
+
if (result.content) {
|
|
1761
|
+
for (const block of result.content) {
|
|
1762
|
+
if (block.type === "text") parts.push(block.text);
|
|
1763
|
+
else if (block.type === "image")
|
|
1764
|
+
parts.push(`[image: ${block.mimeType}, ${Math.ceil(block.data.length * 3 / 4)} bytes]`);
|
|
1765
|
+
else if (block.type === "resource_link") parts.push(`[resource: ${block.uri}]`);
|
|
1766
|
+
else if (block.type === "resource") {
|
|
1767
|
+
parts.push(
|
|
1768
|
+
"text" in block.resource ? block.resource.text : `[blob: ${block.resource.uri}]`
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (parts.length === 0 && result.structuredContent !== void 0) {
|
|
1774
|
+
parts.push(JSON.stringify(result.structuredContent, null, 2));
|
|
1775
|
+
}
|
|
1776
|
+
return parts.join("\n");
|
|
1777
|
+
}
|
|
1778
|
+
async function readDataArg(platform, dataArg) {
|
|
1779
|
+
let raw;
|
|
1780
|
+
if (dataArg.startsWith("@-")) {
|
|
1781
|
+
const reader = platform.stdin.getReader();
|
|
1782
|
+
const chunks = [];
|
|
1783
|
+
for (; ; ) {
|
|
1784
|
+
const { done, value } = await reader.read();
|
|
1785
|
+
if (done) break;
|
|
1786
|
+
chunks.push(value);
|
|
1787
|
+
}
|
|
1788
|
+
reader.releaseLock();
|
|
1789
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
1790
|
+
const merged = new Uint8Array(total);
|
|
1791
|
+
let offset = 0;
|
|
1792
|
+
for (const c of chunks) {
|
|
1793
|
+
merged.set(c, offset);
|
|
1794
|
+
offset += c.length;
|
|
1795
|
+
}
|
|
1796
|
+
raw = new TextDecoder().decode(merged);
|
|
1797
|
+
} else if (dataArg.startsWith("@")) {
|
|
1798
|
+
const data = await platform.fs.readFile(dataArg.slice(1));
|
|
1799
|
+
raw = new TextDecoder().decode(data);
|
|
1800
|
+
} else {
|
|
1801
|
+
raw = dataArg;
|
|
1802
|
+
}
|
|
1803
|
+
return JSON.parse(raw);
|
|
1804
|
+
}
|
|
1805
|
+
async function runInfo(platform, conn) {
|
|
1806
|
+
const info = await conn.info();
|
|
1807
|
+
const lines = [];
|
|
1808
|
+
if (info.name) lines.push(`Server: ${info.name}${info.version ? ` ${info.version}` : ""}`);
|
|
1809
|
+
if (info.title) lines.push(`Title: ${info.title}`);
|
|
1810
|
+
lines.push(`Protocol: ${info.protocolVersion}`);
|
|
1811
|
+
if (info.instructions) lines.push(`Instructions: ${info.instructions}`);
|
|
1812
|
+
const caps = Object.keys(info.capabilities).filter(
|
|
1813
|
+
(k) => info.capabilities[k] != null && info.capabilities[k] !== false
|
|
1814
|
+
);
|
|
1815
|
+
if (caps.length > 0) lines.push(`Capabilities: ${caps.join(", ")}`);
|
|
1816
|
+
try {
|
|
1817
|
+
const tools = await conn.listTools();
|
|
1818
|
+
lines.push(`Tools: ${tools.length}`);
|
|
1819
|
+
} catch {
|
|
1820
|
+
}
|
|
1821
|
+
await platform.write(1, `${lines.join("\n")}
|
|
1822
|
+
`);
|
|
1823
|
+
return 0;
|
|
1824
|
+
}
|
|
1825
|
+
async function runTools(platform, conn) {
|
|
1826
|
+
const tools = await conn.listTools();
|
|
1827
|
+
if (tools.length === 0) {
|
|
1828
|
+
await platform.write(1, "(no tools)\n");
|
|
1829
|
+
} else {
|
|
1830
|
+
await platform.write(1, `${tools.map(formatToolLine).join("\n")}
|
|
1831
|
+
`);
|
|
1832
|
+
}
|
|
1833
|
+
return 0;
|
|
1834
|
+
}
|
|
1835
|
+
async function runCall(platform, conn, toolName, dataArg) {
|
|
1836
|
+
let toolArgs = {};
|
|
1837
|
+
if (dataArg) {
|
|
1838
|
+
try {
|
|
1839
|
+
toolArgs = await readDataArg(platform, dataArg);
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
await platform.write(2, `mcpcat: invalid -d argument: ${err.message}
|
|
1842
|
+
`);
|
|
1843
|
+
return 2;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const result = await conn.callTool(toolName, toolArgs);
|
|
1847
|
+
const output = renderCallResult(result);
|
|
1848
|
+
if (result.isError) {
|
|
1849
|
+
await platform.write(2, `${output}
|
|
1850
|
+
`);
|
|
1851
|
+
return 1;
|
|
1852
|
+
}
|
|
1853
|
+
await platform.write(1, `${output}
|
|
1854
|
+
`);
|
|
1855
|
+
return 0;
|
|
1856
|
+
}
|
|
1857
|
+
async function runDescribe(platform, conn, toolName, showSchema) {
|
|
1858
|
+
const tools = await conn.listTools();
|
|
1859
|
+
if (toolName) {
|
|
1860
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
1861
|
+
if (!tool) {
|
|
1862
|
+
await platform.write(2, `mcpcat: tool '${toolName}' not found
|
|
1863
|
+
`);
|
|
1864
|
+
return 2;
|
|
1865
|
+
}
|
|
1866
|
+
if (showSchema && tool.inputSchema) {
|
|
1867
|
+
await platform.write(1, `${JSON.stringify(tool.inputSchema, null, 2)}
|
|
1868
|
+
`);
|
|
1869
|
+
} else {
|
|
1870
|
+
await platform.write(1, `${formatToolSchema(tool)}
|
|
1871
|
+
`);
|
|
1872
|
+
}
|
|
1873
|
+
} else {
|
|
1874
|
+
for (const tool of tools) {
|
|
1875
|
+
await platform.write(1, `${formatToolSchema(tool)}
|
|
1876
|
+
|
|
1877
|
+
`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return 0;
|
|
1881
|
+
}
|
|
1882
|
+
async function runVerb(platform, parsed, conn) {
|
|
1883
|
+
switch (parsed.verb) {
|
|
1884
|
+
case "info":
|
|
1885
|
+
return runInfo(platform, conn);
|
|
1886
|
+
case "tools":
|
|
1887
|
+
return runTools(platform, conn);
|
|
1888
|
+
case "call":
|
|
1889
|
+
return runCall(platform, conn, parsed.toolName, parsed.dataArg);
|
|
1890
|
+
case "describe":
|
|
1891
|
+
return runDescribe(platform, conn, parsed.toolName, parsed.showSchema);
|
|
1892
|
+
default:
|
|
1893
|
+
await platform.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
1894
|
+
`);
|
|
1895
|
+
return 2;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
async function run(platform, version, args) {
|
|
1899
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
1900
|
+
await platform.write(1, `${version}
|
|
1901
|
+
`);
|
|
1902
|
+
return 0;
|
|
1903
|
+
}
|
|
1904
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
1905
|
+
await platform.write(1, helpText(version));
|
|
1906
|
+
return 0;
|
|
1907
|
+
}
|
|
1908
|
+
if (args.length === 0) {
|
|
1909
|
+
await platform.write(1, helpText(version));
|
|
1910
|
+
return 0;
|
|
1911
|
+
}
|
|
1912
|
+
const parsed = parseArgs(args);
|
|
1913
|
+
if (!parsed.verb) {
|
|
1914
|
+
await platform.write(2, "mcpcat: no verb or target given\n");
|
|
1915
|
+
return 2;
|
|
1916
|
+
}
|
|
1917
|
+
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
1918
|
+
await platform.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
1919
|
+
`);
|
|
1920
|
+
return 1;
|
|
1921
|
+
}
|
|
1922
|
+
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
1923
|
+
await platform.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
1924
|
+
`);
|
|
1925
|
+
return 2;
|
|
1926
|
+
}
|
|
1927
|
+
if (parsed.verb === "call" && !parsed.toolName) {
|
|
1928
|
+
await platform.write(2, "mcpcat: call requires a tool name\n");
|
|
1929
|
+
return 2;
|
|
1930
|
+
}
|
|
1931
|
+
if (!parsed.target) {
|
|
1932
|
+
await platform.write(2, `mcpcat: ${parsed.verb} requires a target
|
|
1933
|
+
`);
|
|
1934
|
+
return 2;
|
|
1935
|
+
}
|
|
1936
|
+
const config = createConfigSource(platform);
|
|
1937
|
+
let resolved;
|
|
1938
|
+
try {
|
|
1939
|
+
resolved = await resolveTarget(
|
|
1940
|
+
parsed.target === "__trailing__" ? "" : parsed.target,
|
|
1941
|
+
parsed.trailingCommand,
|
|
1942
|
+
config
|
|
1943
|
+
);
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
await platform.write(2, `mcpcat: ${err.message}
|
|
1946
|
+
`);
|
|
1947
|
+
return 2;
|
|
1948
|
+
}
|
|
1949
|
+
let conn;
|
|
1950
|
+
try {
|
|
1951
|
+
conn = await connect(platform, {
|
|
1952
|
+
...resolved,
|
|
1953
|
+
headers: { ...resolved.headers, ...parsed.headers },
|
|
1954
|
+
protocolVersion: parsed.protocolVersion,
|
|
1955
|
+
version
|
|
1956
|
+
});
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
await platform.write(2, `mcpcat: connection failed: ${err.message}
|
|
1959
|
+
`);
|
|
1960
|
+
return 3;
|
|
1961
|
+
}
|
|
1962
|
+
try {
|
|
1963
|
+
return await runVerb(platform, parsed, conn);
|
|
1964
|
+
} catch (err) {
|
|
1965
|
+
if (err instanceof InputRequiredError) {
|
|
1966
|
+
await platform.write(2, `${err.requestJson}
|
|
1967
|
+
`);
|
|
1968
|
+
return 6;
|
|
1969
|
+
}
|
|
1970
|
+
await platform.write(2, `mcpcat: ${err.message}
|
|
1971
|
+
`);
|
|
1972
|
+
return 5;
|
|
1973
|
+
} finally {
|
|
1974
|
+
try {
|
|
1975
|
+
await conn.close();
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// src/platform/kernel.ts
|
|
1982
|
+
function kernelFs(ctx) {
|
|
1983
|
+
return {
|
|
1984
|
+
readFile: (path) => ctx.fs.readFile(path),
|
|
1985
|
+
writeFile: (path, data) => ctx.fs.writeFile(path, data),
|
|
1986
|
+
mkdir: (path) => ctx.fs.mkdir(path),
|
|
1987
|
+
rename: (from, to) => ctx.fs.rename(from, to),
|
|
1988
|
+
stat: (path) => ctx.fs.stat(path),
|
|
1989
|
+
exists: (path) => ctx.fs.exists(path)
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
async function kernelSpawn(ctx, options) {
|
|
1993
|
+
const result = await ctx.spawn({
|
|
1994
|
+
argv: options.argv,
|
|
1995
|
+
env: options.env,
|
|
1996
|
+
cwd: options.cwd,
|
|
1997
|
+
stdin: "pipe",
|
|
1998
|
+
stdout: "pipe",
|
|
1999
|
+
stderr: "pipe"
|
|
2000
|
+
});
|
|
2001
|
+
return {
|
|
2002
|
+
stdin: result.stdin,
|
|
2003
|
+
stdout: result.stdout,
|
|
2004
|
+
stderr: result.stderr,
|
|
2005
|
+
wait: () => result.wait(),
|
|
2006
|
+
kill: (signal) => result.kill(signal)
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function createKernelPlatform(ctx) {
|
|
2010
|
+
const encoder = new TextEncoder();
|
|
2011
|
+
return {
|
|
2012
|
+
fetch: ctx.fetch,
|
|
2013
|
+
spawn: (options) => kernelSpawn(ctx, options),
|
|
2014
|
+
fs: kernelFs(ctx),
|
|
2015
|
+
env: ctx.env,
|
|
2016
|
+
cwd: ctx.cwd,
|
|
2017
|
+
isatty: (fd) => ctx.isatty(fd),
|
|
2018
|
+
stdin: ctx.stdin,
|
|
2019
|
+
stdout: ctx.stdout,
|
|
2020
|
+
stderr: ctx.stderr,
|
|
2021
|
+
async write(fd, data) {
|
|
2022
|
+
await ctx.write(fd, typeof data === "string" ? encoder.encode(data) : data);
|
|
2023
|
+
},
|
|
2024
|
+
exit(code) {
|
|
2025
|
+
ctx.exit(code);
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/kernel.ts
|
|
2031
|
+
async function main(ctx) {
|
|
2032
|
+
const platform = createKernelPlatform(ctx);
|
|
2033
|
+
const args = ctx.argv.slice(1);
|
|
2034
|
+
return run(platform, "0.2.0", args);
|
|
2035
|
+
}
|
|
2036
|
+
export {
|
|
2037
|
+
main as default
|
|
2038
|
+
};
|