@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,2626 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __mcpcat_createRequire } from 'node:module';
|
|
3
|
+
const require = __mcpcat_createRequire(import.meta.url);
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
11
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
12
|
+
}) : x)(function(x) {
|
|
13
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
14
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
15
|
+
});
|
|
16
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
17
|
+
try {
|
|
18
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
throw mod = 0, e;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
25
|
+
for (let key of __getOwnPropNames(from))
|
|
26
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
27
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
28
|
+
}
|
|
29
|
+
return to;
|
|
30
|
+
};
|
|
31
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
32
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
33
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
34
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
35
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
36
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
37
|
+
mod
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
// node_modules/isexe/windows.js
|
|
41
|
+
var require_windows = __commonJS({
|
|
42
|
+
"node_modules/isexe/windows.js"(exports, module) {
|
|
43
|
+
module.exports = isexe;
|
|
44
|
+
isexe.sync = sync;
|
|
45
|
+
var fs = __require("fs");
|
|
46
|
+
function checkPathExt(path, options) {
|
|
47
|
+
var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
|
|
48
|
+
if (!pathext) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
pathext = pathext.split(";");
|
|
52
|
+
if (pathext.indexOf("") !== -1) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
for (var i = 0; i < pathext.length; i++) {
|
|
56
|
+
var p = pathext[i].toLowerCase();
|
|
57
|
+
if (p && path.substr(-p.length).toLowerCase() === p) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
function checkStat(stat2, path, options) {
|
|
64
|
+
if (!stat2.isSymbolicLink() && !stat2.isFile()) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return checkPathExt(path, options);
|
|
68
|
+
}
|
|
69
|
+
function isexe(path, options, cb) {
|
|
70
|
+
fs.stat(path, function(er, stat2) {
|
|
71
|
+
cb(er, er ? false : checkStat(stat2, path, options));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function sync(path, options) {
|
|
75
|
+
return checkStat(fs.statSync(path), path, options);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// node_modules/isexe/mode.js
|
|
81
|
+
var require_mode = __commonJS({
|
|
82
|
+
"node_modules/isexe/mode.js"(exports, module) {
|
|
83
|
+
module.exports = isexe;
|
|
84
|
+
isexe.sync = sync;
|
|
85
|
+
var fs = __require("fs");
|
|
86
|
+
function isexe(path, options, cb) {
|
|
87
|
+
fs.stat(path, function(er, stat2) {
|
|
88
|
+
cb(er, er ? false : checkStat(stat2, options));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function sync(path, options) {
|
|
92
|
+
return checkStat(fs.statSync(path), options);
|
|
93
|
+
}
|
|
94
|
+
function checkStat(stat2, options) {
|
|
95
|
+
return stat2.isFile() && checkMode(stat2, options);
|
|
96
|
+
}
|
|
97
|
+
function checkMode(stat2, options) {
|
|
98
|
+
var mod = stat2.mode;
|
|
99
|
+
var uid = stat2.uid;
|
|
100
|
+
var gid = stat2.gid;
|
|
101
|
+
var myUid = options.uid !== void 0 ? options.uid : process.getuid && process.getuid();
|
|
102
|
+
var myGid = options.gid !== void 0 ? options.gid : process.getgid && process.getgid();
|
|
103
|
+
var u = parseInt("100", 8);
|
|
104
|
+
var g = parseInt("010", 8);
|
|
105
|
+
var o = parseInt("001", 8);
|
|
106
|
+
var ug = u | g;
|
|
107
|
+
var ret = mod & o || mod & g && gid === myGid || mod & u && uid === myUid || mod & ug && myUid === 0;
|
|
108
|
+
return ret;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// node_modules/isexe/index.js
|
|
114
|
+
var require_isexe = __commonJS({
|
|
115
|
+
"node_modules/isexe/index.js"(exports, module) {
|
|
116
|
+
var fs = __require("fs");
|
|
117
|
+
var core;
|
|
118
|
+
if (process.platform === "win32" || global.TESTING_WINDOWS) {
|
|
119
|
+
core = require_windows();
|
|
120
|
+
} else {
|
|
121
|
+
core = require_mode();
|
|
122
|
+
}
|
|
123
|
+
module.exports = isexe;
|
|
124
|
+
isexe.sync = sync;
|
|
125
|
+
function isexe(path, options, cb) {
|
|
126
|
+
if (typeof options === "function") {
|
|
127
|
+
cb = options;
|
|
128
|
+
options = {};
|
|
129
|
+
}
|
|
130
|
+
if (!cb) {
|
|
131
|
+
if (typeof Promise !== "function") {
|
|
132
|
+
throw new TypeError("callback not provided");
|
|
133
|
+
}
|
|
134
|
+
return new Promise(function(resolve, reject) {
|
|
135
|
+
isexe(path, options || {}, function(er, is) {
|
|
136
|
+
if (er) {
|
|
137
|
+
reject(er);
|
|
138
|
+
} else {
|
|
139
|
+
resolve(is);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
core(path, options || {}, function(er, is) {
|
|
145
|
+
if (er) {
|
|
146
|
+
if (er.code === "EACCES" || options && options.ignoreErrors) {
|
|
147
|
+
er = null;
|
|
148
|
+
is = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
cb(er, is);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function sync(path, options) {
|
|
155
|
+
try {
|
|
156
|
+
return core.sync(path, options || {});
|
|
157
|
+
} catch (er) {
|
|
158
|
+
if (options && options.ignoreErrors || er.code === "EACCES") {
|
|
159
|
+
return false;
|
|
160
|
+
} else {
|
|
161
|
+
throw er;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// node_modules/which/which.js
|
|
169
|
+
var require_which = __commonJS({
|
|
170
|
+
"node_modules/which/which.js"(exports, module) {
|
|
171
|
+
var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
|
|
172
|
+
var path = __require("path");
|
|
173
|
+
var COLON = isWindows ? ";" : ":";
|
|
174
|
+
var isexe = require_isexe();
|
|
175
|
+
var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
|
|
176
|
+
var getPathInfo = (cmd, opt) => {
|
|
177
|
+
const colon = opt.colon || COLON;
|
|
178
|
+
const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? [""] : [
|
|
179
|
+
// windows always checks the cwd first
|
|
180
|
+
...isWindows ? [process.cwd()] : [],
|
|
181
|
+
...(opt.path || process.env.PATH || /* istanbul ignore next: very unusual */
|
|
182
|
+
"").split(colon)
|
|
183
|
+
];
|
|
184
|
+
const pathExtExe = isWindows ? opt.pathExt || process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM" : "";
|
|
185
|
+
const pathExt = isWindows ? pathExtExe.split(colon) : [""];
|
|
186
|
+
if (isWindows) {
|
|
187
|
+
if (cmd.indexOf(".") !== -1 && pathExt[0] !== "")
|
|
188
|
+
pathExt.unshift("");
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
pathEnv,
|
|
192
|
+
pathExt,
|
|
193
|
+
pathExtExe
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
var which = (cmd, opt, cb) => {
|
|
197
|
+
if (typeof opt === "function") {
|
|
198
|
+
cb = opt;
|
|
199
|
+
opt = {};
|
|
200
|
+
}
|
|
201
|
+
if (!opt)
|
|
202
|
+
opt = {};
|
|
203
|
+
const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
|
|
204
|
+
const found = [];
|
|
205
|
+
const step = (i) => new Promise((resolve, reject) => {
|
|
206
|
+
if (i === pathEnv.length)
|
|
207
|
+
return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
|
|
208
|
+
const ppRaw = pathEnv[i];
|
|
209
|
+
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
210
|
+
const pCmd = path.join(pathPart, cmd);
|
|
211
|
+
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
212
|
+
resolve(subStep(p, i, 0));
|
|
213
|
+
});
|
|
214
|
+
const subStep = (p, i, ii) => new Promise((resolve, reject) => {
|
|
215
|
+
if (ii === pathExt.length)
|
|
216
|
+
return resolve(step(i + 1));
|
|
217
|
+
const ext = pathExt[ii];
|
|
218
|
+
isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
|
|
219
|
+
if (!er && is) {
|
|
220
|
+
if (opt.all)
|
|
221
|
+
found.push(p + ext);
|
|
222
|
+
else
|
|
223
|
+
return resolve(p + ext);
|
|
224
|
+
}
|
|
225
|
+
return resolve(subStep(p, i, ii + 1));
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
|
|
229
|
+
};
|
|
230
|
+
var whichSync = (cmd, opt) => {
|
|
231
|
+
opt = opt || {};
|
|
232
|
+
const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
|
|
233
|
+
const found = [];
|
|
234
|
+
for (let i = 0; i < pathEnv.length; i++) {
|
|
235
|
+
const ppRaw = pathEnv[i];
|
|
236
|
+
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
237
|
+
const pCmd = path.join(pathPart, cmd);
|
|
238
|
+
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
239
|
+
for (let j = 0; j < pathExt.length; j++) {
|
|
240
|
+
const cur = p + pathExt[j];
|
|
241
|
+
try {
|
|
242
|
+
const is = isexe.sync(cur, { pathExt: pathExtExe });
|
|
243
|
+
if (is) {
|
|
244
|
+
if (opt.all)
|
|
245
|
+
found.push(cur);
|
|
246
|
+
else
|
|
247
|
+
return cur;
|
|
248
|
+
}
|
|
249
|
+
} catch (ex) {
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (opt.all && found.length)
|
|
254
|
+
return found;
|
|
255
|
+
if (opt.nothrow)
|
|
256
|
+
return null;
|
|
257
|
+
throw getNotFoundError(cmd);
|
|
258
|
+
};
|
|
259
|
+
module.exports = which;
|
|
260
|
+
which.sync = whichSync;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// node_modules/path-key/index.js
|
|
265
|
+
var require_path_key = __commonJS({
|
|
266
|
+
"node_modules/path-key/index.js"(exports, module) {
|
|
267
|
+
"use strict";
|
|
268
|
+
var pathKey = (options = {}) => {
|
|
269
|
+
const environment = options.env || process.env;
|
|
270
|
+
const platform2 = options.platform || process.platform;
|
|
271
|
+
if (platform2 !== "win32") {
|
|
272
|
+
return "PATH";
|
|
273
|
+
}
|
|
274
|
+
return Object.keys(environment).reverse().find((key) => key.toUpperCase() === "PATH") || "Path";
|
|
275
|
+
};
|
|
276
|
+
module.exports = pathKey;
|
|
277
|
+
module.exports.default = pathKey;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// node_modules/cross-spawn/lib/util/resolveCommand.js
|
|
282
|
+
var require_resolveCommand = __commonJS({
|
|
283
|
+
"node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) {
|
|
284
|
+
"use strict";
|
|
285
|
+
var path = __require("path");
|
|
286
|
+
var which = require_which();
|
|
287
|
+
var getPathKey = require_path_key();
|
|
288
|
+
function resolveCommandAttempt(parsed, withoutPathExt) {
|
|
289
|
+
const env = parsed.options.env || process.env;
|
|
290
|
+
const cwd = process.cwd();
|
|
291
|
+
const hasCustomCwd = parsed.options.cwd != null;
|
|
292
|
+
const shouldSwitchCwd = hasCustomCwd && process.chdir !== void 0 && !process.chdir.disabled;
|
|
293
|
+
if (shouldSwitchCwd) {
|
|
294
|
+
try {
|
|
295
|
+
process.chdir(parsed.options.cwd);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
let resolved;
|
|
300
|
+
try {
|
|
301
|
+
resolved = which.sync(parsed.command, {
|
|
302
|
+
path: env[getPathKey({ env })],
|
|
303
|
+
pathExt: withoutPathExt ? path.delimiter : void 0
|
|
304
|
+
});
|
|
305
|
+
} catch (e) {
|
|
306
|
+
} finally {
|
|
307
|
+
if (shouldSwitchCwd) {
|
|
308
|
+
process.chdir(cwd);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (resolved) {
|
|
312
|
+
resolved = path.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
|
|
313
|
+
}
|
|
314
|
+
return resolved;
|
|
315
|
+
}
|
|
316
|
+
function resolveCommand(parsed) {
|
|
317
|
+
return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true);
|
|
318
|
+
}
|
|
319
|
+
module.exports = resolveCommand;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// node_modules/cross-spawn/lib/util/escape.js
|
|
324
|
+
var require_escape = __commonJS({
|
|
325
|
+
"node_modules/cross-spawn/lib/util/escape.js"(exports, module) {
|
|
326
|
+
"use strict";
|
|
327
|
+
var metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g;
|
|
328
|
+
function escapeCommand(arg) {
|
|
329
|
+
arg = arg.replace(metaCharsRegExp, "^$1");
|
|
330
|
+
return arg;
|
|
331
|
+
}
|
|
332
|
+
function escapeArgument(arg, doubleEscapeMetaChars) {
|
|
333
|
+
arg = `${arg}`;
|
|
334
|
+
arg = arg.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
|
|
335
|
+
arg = arg.replace(/(?=(\\+?)?)\1$/, "$1$1");
|
|
336
|
+
arg = `"${arg}"`;
|
|
337
|
+
arg = arg.replace(metaCharsRegExp, "^$1");
|
|
338
|
+
if (doubleEscapeMetaChars) {
|
|
339
|
+
arg = arg.replace(metaCharsRegExp, "^$1");
|
|
340
|
+
}
|
|
341
|
+
return arg;
|
|
342
|
+
}
|
|
343
|
+
module.exports.command = escapeCommand;
|
|
344
|
+
module.exports.argument = escapeArgument;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// node_modules/shebang-regex/index.js
|
|
349
|
+
var require_shebang_regex = __commonJS({
|
|
350
|
+
"node_modules/shebang-regex/index.js"(exports, module) {
|
|
351
|
+
"use strict";
|
|
352
|
+
module.exports = /^#!(.*)/;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// node_modules/shebang-command/index.js
|
|
357
|
+
var require_shebang_command = __commonJS({
|
|
358
|
+
"node_modules/shebang-command/index.js"(exports, module) {
|
|
359
|
+
"use strict";
|
|
360
|
+
var shebangRegex = require_shebang_regex();
|
|
361
|
+
module.exports = (string = "") => {
|
|
362
|
+
const match = string.match(shebangRegex);
|
|
363
|
+
if (!match) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const [path, argument] = match[0].replace(/#! ?/, "").split(" ");
|
|
367
|
+
const binary = path.split("/").pop();
|
|
368
|
+
if (binary === "env") {
|
|
369
|
+
return argument;
|
|
370
|
+
}
|
|
371
|
+
return argument ? `${binary} ${argument}` : binary;
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// node_modules/cross-spawn/lib/util/readShebang.js
|
|
377
|
+
var require_readShebang = __commonJS({
|
|
378
|
+
"node_modules/cross-spawn/lib/util/readShebang.js"(exports, module) {
|
|
379
|
+
"use strict";
|
|
380
|
+
var fs = __require("fs");
|
|
381
|
+
var shebangCommand = require_shebang_command();
|
|
382
|
+
function readShebang(command) {
|
|
383
|
+
const size = 150;
|
|
384
|
+
const buffer = Buffer.alloc(size);
|
|
385
|
+
let fd;
|
|
386
|
+
try {
|
|
387
|
+
fd = fs.openSync(command, "r");
|
|
388
|
+
fs.readSync(fd, buffer, 0, size, 0);
|
|
389
|
+
fs.closeSync(fd);
|
|
390
|
+
} catch (e) {
|
|
391
|
+
}
|
|
392
|
+
return shebangCommand(buffer.toString());
|
|
393
|
+
}
|
|
394
|
+
module.exports = readShebang;
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// node_modules/cross-spawn/lib/parse.js
|
|
399
|
+
var require_parse = __commonJS({
|
|
400
|
+
"node_modules/cross-spawn/lib/parse.js"(exports, module) {
|
|
401
|
+
"use strict";
|
|
402
|
+
var path = __require("path");
|
|
403
|
+
var resolveCommand = require_resolveCommand();
|
|
404
|
+
var escape = require_escape();
|
|
405
|
+
var readShebang = require_readShebang();
|
|
406
|
+
var isWin = process.platform === "win32";
|
|
407
|
+
var isExecutableRegExp = /\.(?:com|exe)$/i;
|
|
408
|
+
var isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
|
|
409
|
+
function detectShebang(parsed) {
|
|
410
|
+
parsed.file = resolveCommand(parsed);
|
|
411
|
+
const shebang = parsed.file && readShebang(parsed.file);
|
|
412
|
+
if (shebang) {
|
|
413
|
+
parsed.args.unshift(parsed.file);
|
|
414
|
+
parsed.command = shebang;
|
|
415
|
+
return resolveCommand(parsed);
|
|
416
|
+
}
|
|
417
|
+
return parsed.file;
|
|
418
|
+
}
|
|
419
|
+
function parseNonShell(parsed) {
|
|
420
|
+
if (!isWin) {
|
|
421
|
+
return parsed;
|
|
422
|
+
}
|
|
423
|
+
const commandFile = detectShebang(parsed);
|
|
424
|
+
const needsShell = !isExecutableRegExp.test(commandFile);
|
|
425
|
+
if (parsed.options.forceShell || needsShell) {
|
|
426
|
+
const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
|
|
427
|
+
parsed.command = path.normalize(parsed.command);
|
|
428
|
+
parsed.command = escape.command(parsed.command);
|
|
429
|
+
parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
|
|
430
|
+
const shellCommand = [parsed.command].concat(parsed.args).join(" ");
|
|
431
|
+
parsed.args = ["/d", "/s", "/c", `"${shellCommand}"`];
|
|
432
|
+
parsed.command = process.env.comspec || "cmd.exe";
|
|
433
|
+
parsed.options.windowsVerbatimArguments = true;
|
|
434
|
+
}
|
|
435
|
+
return parsed;
|
|
436
|
+
}
|
|
437
|
+
function parse(command, args2, options) {
|
|
438
|
+
if (args2 && !Array.isArray(args2)) {
|
|
439
|
+
options = args2;
|
|
440
|
+
args2 = null;
|
|
441
|
+
}
|
|
442
|
+
args2 = args2 ? args2.slice(0) : [];
|
|
443
|
+
options = Object.assign({}, options);
|
|
444
|
+
const parsed = {
|
|
445
|
+
command,
|
|
446
|
+
args: args2,
|
|
447
|
+
options,
|
|
448
|
+
file: void 0,
|
|
449
|
+
original: {
|
|
450
|
+
command,
|
|
451
|
+
args: args2
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
return options.shell ? parsed : parseNonShell(parsed);
|
|
455
|
+
}
|
|
456
|
+
module.exports = parse;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// node_modules/cross-spawn/lib/enoent.js
|
|
461
|
+
var require_enoent = __commonJS({
|
|
462
|
+
"node_modules/cross-spawn/lib/enoent.js"(exports, module) {
|
|
463
|
+
"use strict";
|
|
464
|
+
var isWin = process.platform === "win32";
|
|
465
|
+
function notFoundError(original, syscall) {
|
|
466
|
+
return Object.assign(new Error(`${syscall} ${original.command} ENOENT`), {
|
|
467
|
+
code: "ENOENT",
|
|
468
|
+
errno: "ENOENT",
|
|
469
|
+
syscall: `${syscall} ${original.command}`,
|
|
470
|
+
path: original.command,
|
|
471
|
+
spawnargs: original.args
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function hookChildProcess(cp, parsed) {
|
|
475
|
+
if (!isWin) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const originalEmit = cp.emit;
|
|
479
|
+
cp.emit = function(name, arg1) {
|
|
480
|
+
if (name === "exit") {
|
|
481
|
+
const err = verifyENOENT(arg1, parsed);
|
|
482
|
+
if (err) {
|
|
483
|
+
return originalEmit.call(cp, "error", err);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return originalEmit.apply(cp, arguments);
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function verifyENOENT(status, parsed) {
|
|
490
|
+
if (isWin && status === 1 && !parsed.file) {
|
|
491
|
+
return notFoundError(parsed.original, "spawn");
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
function verifyENOENTSync(status, parsed) {
|
|
496
|
+
if (isWin && status === 1 && !parsed.file) {
|
|
497
|
+
return notFoundError(parsed.original, "spawnSync");
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
module.exports = {
|
|
502
|
+
hookChildProcess,
|
|
503
|
+
verifyENOENT,
|
|
504
|
+
verifyENOENTSync,
|
|
505
|
+
notFoundError
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// node_modules/cross-spawn/index.js
|
|
511
|
+
var require_cross_spawn = __commonJS({
|
|
512
|
+
"node_modules/cross-spawn/index.js"(exports, module) {
|
|
513
|
+
"use strict";
|
|
514
|
+
var cp = __require("child_process");
|
|
515
|
+
var parse = require_parse();
|
|
516
|
+
var enoent = require_enoent();
|
|
517
|
+
function spawn2(command, args2, options) {
|
|
518
|
+
const parsed = parse(command, args2, options);
|
|
519
|
+
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
520
|
+
enoent.hookChildProcess(spawned, parsed);
|
|
521
|
+
return spawned;
|
|
522
|
+
}
|
|
523
|
+
function spawnSync(command, args2, options) {
|
|
524
|
+
const parsed = parse(command, args2, options);
|
|
525
|
+
const result = cp.spawnSync(parsed.command, parsed.args, parsed.options);
|
|
526
|
+
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
module.exports = spawn2;
|
|
530
|
+
module.exports.spawn = spawn2;
|
|
531
|
+
module.exports.sync = spawnSync;
|
|
532
|
+
module.exports._parse = parse;
|
|
533
|
+
module.exports._enoent = enoent;
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// node_modules/@earendil-works/pi-mcp/dist/protocol/jsonrpc.js
|
|
538
|
+
var JSON_RPC_ERROR_CODES = {
|
|
539
|
+
parseError: -32700,
|
|
540
|
+
invalidRequest: -32600,
|
|
541
|
+
methodNotFound: -32601,
|
|
542
|
+
invalidParams: -32602,
|
|
543
|
+
internalError: -32603
|
|
544
|
+
};
|
|
545
|
+
var McpError = class extends Error {
|
|
546
|
+
code;
|
|
547
|
+
data;
|
|
548
|
+
constructor(code2, message, data) {
|
|
549
|
+
super(message);
|
|
550
|
+
this.name = "McpError";
|
|
551
|
+
this.code = code2;
|
|
552
|
+
this.data = data;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
var McpConnectionClosedError = class extends Error {
|
|
556
|
+
constructor(message = "MCP connection closed") {
|
|
557
|
+
super(message);
|
|
558
|
+
this.name = "McpConnectionClosedError";
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
var McpTimeoutError = class extends Error {
|
|
562
|
+
timeoutMs;
|
|
563
|
+
constructor(timeoutMs) {
|
|
564
|
+
super(`MCP request timed out after ${timeoutMs}ms`);
|
|
565
|
+
this.name = "McpTimeoutError";
|
|
566
|
+
this.timeoutMs = timeoutMs;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
var McpAbortError = class extends Error {
|
|
570
|
+
constructor(message = "MCP request aborted") {
|
|
571
|
+
super(message);
|
|
572
|
+
this.name = "AbortError";
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
function isObject(value) {
|
|
576
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
577
|
+
}
|
|
578
|
+
function toError(value) {
|
|
579
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
580
|
+
}
|
|
581
|
+
function isJsonRpcId(value) {
|
|
582
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
|
|
583
|
+
}
|
|
584
|
+
function isJsonRpcRequest(message) {
|
|
585
|
+
return isObject(message) && message.jsonrpc === "2.0" && isJsonRpcId(message.id) && typeof message.method === "string";
|
|
586
|
+
}
|
|
587
|
+
function isJsonRpcNotification(message) {
|
|
588
|
+
return isObject(message) && message.jsonrpc === "2.0" && !("id" in message) && typeof message.method === "string";
|
|
589
|
+
}
|
|
590
|
+
function isJsonRpcResponse(message) {
|
|
591
|
+
if (!isObject(message) || message.jsonrpc !== "2.0" || !isJsonRpcId(message.id))
|
|
592
|
+
return false;
|
|
593
|
+
if ("result" in message)
|
|
594
|
+
return !("error" in message);
|
|
595
|
+
if (!("error" in message) || !isObject(message.error))
|
|
596
|
+
return false;
|
|
597
|
+
return typeof message.error.code === "number" && typeof message.error.message === "string";
|
|
598
|
+
}
|
|
599
|
+
function parseJsonRpcMessage(value) {
|
|
600
|
+
if (isJsonRpcRequest(value) || isJsonRpcNotification(value) || isJsonRpcResponse(value))
|
|
601
|
+
return value;
|
|
602
|
+
throw new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid JSON-RPC message");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// node_modules/@earendil-works/pi-mcp/dist/protocol/types.js
|
|
606
|
+
var LATEST_PROTOCOL_VERSION = "2025-11-25";
|
|
607
|
+
var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
608
|
+
|
|
609
|
+
// node_modules/@earendil-works/pi-mcp/dist/client.js
|
|
610
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
611
|
+
var MAX_LIST_PAGES = 1e3;
|
|
612
|
+
function validateInitializeResult(value) {
|
|
613
|
+
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") {
|
|
614
|
+
throw new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid MCP initialize result");
|
|
615
|
+
}
|
|
616
|
+
return value;
|
|
617
|
+
}
|
|
618
|
+
function invalid(message) {
|
|
619
|
+
return new McpError(JSON_RPC_ERROR_CODES.invalidRequest, message);
|
|
620
|
+
}
|
|
621
|
+
function validateListPage(method, key, value, isItem) {
|
|
622
|
+
const items = isObject(value) ? value[key] : void 0;
|
|
623
|
+
if (!isObject(value) || !Array.isArray(items))
|
|
624
|
+
throw invalid(`Invalid MCP ${method} result`);
|
|
625
|
+
for (const item of items) {
|
|
626
|
+
if (!isObject(item) || !isItem(item))
|
|
627
|
+
throw invalid(`Invalid entry in MCP ${method} result`);
|
|
628
|
+
}
|
|
629
|
+
const nextCursor = value.nextCursor === null || value.nextCursor === "" ? void 0 : value.nextCursor;
|
|
630
|
+
if (nextCursor !== void 0 && typeof nextCursor !== "string")
|
|
631
|
+
throw invalid(`Invalid MCP ${method} cursor`);
|
|
632
|
+
return { items, ...nextCursor === void 0 ? {} : { nextCursor } };
|
|
633
|
+
}
|
|
634
|
+
var isTool = (tool) => typeof tool.name === "string" && isObject(tool.inputSchema);
|
|
635
|
+
var isResource = (resource) => typeof resource.uri === "string" && (resource.name === void 0 || typeof resource.name === "string");
|
|
636
|
+
var isResourceTemplate = (template) => typeof template.uriTemplate === "string" && (template.name === void 0 || typeof template.name === "string");
|
|
637
|
+
function toResource(item) {
|
|
638
|
+
return { ...item, name: item.name ?? item.uri };
|
|
639
|
+
}
|
|
640
|
+
function toResourceTemplate(item) {
|
|
641
|
+
return { ...item, name: item.name ?? item.uriTemplate };
|
|
642
|
+
}
|
|
643
|
+
function pageCursor(page) {
|
|
644
|
+
return page.nextCursor === void 0 ? {} : { nextCursor: page.nextCursor };
|
|
645
|
+
}
|
|
646
|
+
function validateReadResourceResult(value) {
|
|
647
|
+
if (!isObject(value) || !Array.isArray(value.contents))
|
|
648
|
+
throw invalid("Invalid MCP resources/read result");
|
|
649
|
+
for (const contents of value.contents) {
|
|
650
|
+
if (!isObject(contents) || typeof contents.uri !== "string" || typeof contents.text !== "string" && typeof contents.blob !== "string") {
|
|
651
|
+
throw invalid("Invalid contents in MCP resources/read result");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
function validateCallToolResult(value) {
|
|
657
|
+
if (!isObject(value) || value.content !== void 0 && !Array.isArray(value.content)) {
|
|
658
|
+
throw new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Invalid MCP tools/call result");
|
|
659
|
+
}
|
|
660
|
+
return value.content === void 0 ? { ...value, content: [] } : value;
|
|
661
|
+
}
|
|
662
|
+
var McpClient = class {
|
|
663
|
+
options;
|
|
664
|
+
state = "idle";
|
|
665
|
+
transport;
|
|
666
|
+
nextRequestId = 1;
|
|
667
|
+
serverInfoValue;
|
|
668
|
+
serverCapabilitiesValue;
|
|
669
|
+
instructionsValue;
|
|
670
|
+
protocolVersionValue;
|
|
671
|
+
pending = /* @__PURE__ */ new Map();
|
|
672
|
+
progressRequests = /* @__PURE__ */ new Map();
|
|
673
|
+
incoming = /* @__PURE__ */ new Map();
|
|
674
|
+
requestHandlers = /* @__PURE__ */ new Map();
|
|
675
|
+
notificationListeners = /* @__PURE__ */ new Map();
|
|
676
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
677
|
+
closeListeners = /* @__PURE__ */ new Set();
|
|
678
|
+
disposers = [];
|
|
679
|
+
constructor(options) {
|
|
680
|
+
this.options = Object.freeze({ ...options });
|
|
681
|
+
this.requestHandlers.set("ping", () => ({}));
|
|
682
|
+
const roots = options.roots;
|
|
683
|
+
if (roots) {
|
|
684
|
+
this.requestHandlers.set("roots/list", async () => ({
|
|
685
|
+
roots: [...typeof roots === "function" ? await roots() : roots]
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
get connectionState() {
|
|
690
|
+
return this.state;
|
|
691
|
+
}
|
|
692
|
+
get serverInfo() {
|
|
693
|
+
return this.serverInfoValue;
|
|
694
|
+
}
|
|
695
|
+
get serverCapabilities() {
|
|
696
|
+
return this.serverCapabilitiesValue;
|
|
697
|
+
}
|
|
698
|
+
get instructions() {
|
|
699
|
+
return this.instructionsValue;
|
|
700
|
+
}
|
|
701
|
+
get protocolVersion() {
|
|
702
|
+
return this.protocolVersionValue;
|
|
703
|
+
}
|
|
704
|
+
async connect(transport) {
|
|
705
|
+
if (this.state !== "idle")
|
|
706
|
+
throw new Error(`Cannot connect MCP client in ${this.state} state`);
|
|
707
|
+
this.state = "connecting";
|
|
708
|
+
this.transport = transport;
|
|
709
|
+
this.disposers = [
|
|
710
|
+
transport.onMessage((message) => this.handleMessage(message)),
|
|
711
|
+
// Transport errors are reported only. Pending requests fail when the transport closes.
|
|
712
|
+
transport.onError((error) => this.emitError(error)),
|
|
713
|
+
transport.onClose(() => this.handleTransportClose())
|
|
714
|
+
];
|
|
715
|
+
try {
|
|
716
|
+
await transport.start();
|
|
717
|
+
const capabilities = { ...this.options.capabilities };
|
|
718
|
+
if (this.options.roots && capabilities.roots === void 0)
|
|
719
|
+
capabilities.roots = {};
|
|
720
|
+
const result = validateInitializeResult(await this.requestInternal("initialize", {
|
|
721
|
+
protocolVersion: this.options.protocolVersion ?? LATEST_PROTOCOL_VERSION,
|
|
722
|
+
capabilities,
|
|
723
|
+
clientInfo: {
|
|
724
|
+
name: this.options.name,
|
|
725
|
+
version: this.options.version,
|
|
726
|
+
...this.options.title === void 0 ? {} : { title: this.options.title }
|
|
727
|
+
}
|
|
728
|
+
}, {}, true));
|
|
729
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
730
|
+
throw new Error(`MCP server selected unsupported protocol version ${result.protocolVersion}`);
|
|
731
|
+
}
|
|
732
|
+
this.protocolVersionValue = result.protocolVersion;
|
|
733
|
+
this.serverInfoValue = result.serverInfo;
|
|
734
|
+
this.serverCapabilitiesValue = result.capabilities;
|
|
735
|
+
this.instructionsValue = result.instructions;
|
|
736
|
+
transport.setProtocolVersion?.(result.protocolVersion);
|
|
737
|
+
await this.notifyInternal("notifications/initialized", void 0, true);
|
|
738
|
+
this.state = "connected";
|
|
739
|
+
return result;
|
|
740
|
+
} catch (error) {
|
|
741
|
+
await this.close().catch(() => {
|
|
742
|
+
});
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
request(method, params, options = {}) {
|
|
747
|
+
return this.requestInternal(method, params, options, false);
|
|
748
|
+
}
|
|
749
|
+
notify(method, params) {
|
|
750
|
+
return this.notifyInternal(method, params, false);
|
|
751
|
+
}
|
|
752
|
+
setRequestHandler(method, handler) {
|
|
753
|
+
this.requestHandlers.set(method, handler);
|
|
754
|
+
return () => {
|
|
755
|
+
if (this.requestHandlers.get(method) === handler)
|
|
756
|
+
this.requestHandlers.delete(method);
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
onNotification(method, listener) {
|
|
760
|
+
const listeners = this.notificationListeners.get(method) ?? /* @__PURE__ */ new Set();
|
|
761
|
+
this.notificationListeners.set(method, listeners);
|
|
762
|
+
listeners.add(listener);
|
|
763
|
+
return () => {
|
|
764
|
+
listeners.delete(listener);
|
|
765
|
+
if (listeners.size === 0)
|
|
766
|
+
this.notificationListeners.delete(method);
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
onError(listener) {
|
|
770
|
+
this.errorListeners.add(listener);
|
|
771
|
+
return () => this.errorListeners.delete(listener);
|
|
772
|
+
}
|
|
773
|
+
/** Called once when the connection closes, whether the transport dropped or `close()` was called. */
|
|
774
|
+
onClose(listener) {
|
|
775
|
+
this.closeListeners.add(listener);
|
|
776
|
+
return () => this.closeListeners.delete(listener);
|
|
777
|
+
}
|
|
778
|
+
async ping(options = {}) {
|
|
779
|
+
await this.request("ping", void 0, options);
|
|
780
|
+
}
|
|
781
|
+
async listTools(options = {}) {
|
|
782
|
+
return await this.listAll("tools/list", "tools", isTool, options);
|
|
783
|
+
}
|
|
784
|
+
/** Every resource, following `nextCursor` through all pages. */
|
|
785
|
+
async listResources(options = {}) {
|
|
786
|
+
return (await this.listAll("resources/list", "resources", isResource, options)).map(toResource);
|
|
787
|
+
}
|
|
788
|
+
/** One page of resources, starting at `cursor`. */
|
|
789
|
+
async listResourcesPage(cursor, options = {}) {
|
|
790
|
+
const page = await this.listPage("resources/list", "resources", isResource, cursor, options);
|
|
791
|
+
return { resources: page.items.map(toResource), ...pageCursor(page) };
|
|
792
|
+
}
|
|
793
|
+
/** Every resource template, following `nextCursor` through all pages. */
|
|
794
|
+
async listResourceTemplates(options = {}) {
|
|
795
|
+
const templates = await this.listAll("resources/templates/list", "resourceTemplates", isResourceTemplate, options);
|
|
796
|
+
return templates.map(toResourceTemplate);
|
|
797
|
+
}
|
|
798
|
+
/** One page of resource templates, starting at `cursor`. */
|
|
799
|
+
async listResourceTemplatesPage(cursor, options = {}) {
|
|
800
|
+
const page = await this.listPage("resources/templates/list", "resourceTemplates", isResourceTemplate, cursor, options);
|
|
801
|
+
return { resourceTemplates: page.items.map(toResourceTemplate), ...pageCursor(page) };
|
|
802
|
+
}
|
|
803
|
+
async readResource(uri, options = {}) {
|
|
804
|
+
return validateReadResourceResult(await this.request("resources/read", { uri }, options));
|
|
805
|
+
}
|
|
806
|
+
async listPage(method, key, isItem, cursor, options) {
|
|
807
|
+
return validateListPage(method, key, await this.request(method, cursor === void 0 ? void 0 : { cursor }, options), isItem);
|
|
808
|
+
}
|
|
809
|
+
/** Every item of a paginated list method. */
|
|
810
|
+
async listAll(method, key, isItem, options) {
|
|
811
|
+
const items = [];
|
|
812
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
813
|
+
let cursor;
|
|
814
|
+
for (let pageNumber = 0; pageNumber < MAX_LIST_PAGES; pageNumber++) {
|
|
815
|
+
const page = await this.listPage(method, key, isItem, cursor, options);
|
|
816
|
+
items.push(...page.items);
|
|
817
|
+
if (page.nextCursor === void 0)
|
|
818
|
+
return items;
|
|
819
|
+
if (cursors.has(page.nextCursor))
|
|
820
|
+
throw new Error(`MCP ${method} returned duplicate cursor: ${page.nextCursor}`);
|
|
821
|
+
cursors.add(page.nextCursor);
|
|
822
|
+
cursor = page.nextCursor;
|
|
823
|
+
}
|
|
824
|
+
throw new Error(`MCP ${method} exceeded ${MAX_LIST_PAGES} pages`);
|
|
825
|
+
}
|
|
826
|
+
async callTool(name, args2, options = {}) {
|
|
827
|
+
return validateCallToolResult(await this.request("tools/call", { name, ...args2 === void 0 ? {} : { arguments: args2 } }, options));
|
|
828
|
+
}
|
|
829
|
+
async close() {
|
|
830
|
+
const transport = this.transport;
|
|
831
|
+
this.transport = void 0;
|
|
832
|
+
this.disposeTransportListeners();
|
|
833
|
+
this.markClosed(new McpConnectionClosedError());
|
|
834
|
+
await transport?.close();
|
|
835
|
+
}
|
|
836
|
+
async requestInternal(method, params, options, allowConnecting) {
|
|
837
|
+
const transport = this.requireTransport(allowConnecting);
|
|
838
|
+
if (options.signal?.aborted)
|
|
839
|
+
throw new McpAbortError();
|
|
840
|
+
const id = this.nextRequestId++;
|
|
841
|
+
const progressToken = options.onProgress ? id : void 0;
|
|
842
|
+
const requestParams = progressToken === void 0 ? params : { ...params, _meta: { ...isObject(params?._meta) ? params._meta : {}, progressToken } };
|
|
843
|
+
const message = {
|
|
844
|
+
jsonrpc: "2.0",
|
|
845
|
+
id,
|
|
846
|
+
method,
|
|
847
|
+
...requestParams === void 0 ? {} : { params: requestParams }
|
|
848
|
+
};
|
|
849
|
+
return new Promise((resolve, reject) => {
|
|
850
|
+
const entry = {
|
|
851
|
+
resolve,
|
|
852
|
+
reject,
|
|
853
|
+
timeoutMs: options.timeoutMs ?? this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
854
|
+
timer: void 0,
|
|
855
|
+
signal: options.signal,
|
|
856
|
+
// The spec forbids cancelling `initialize`.
|
|
857
|
+
onAbort: () => this.cancelPending(id, new McpAbortError(), method !== "initialize", String(options.signal?.reason ?? "Aborted")),
|
|
858
|
+
sendController: new AbortController(),
|
|
859
|
+
cancellable: method !== "initialize",
|
|
860
|
+
onProgress: options.onProgress,
|
|
861
|
+
progressToken
|
|
862
|
+
};
|
|
863
|
+
this.pending.set(id, entry);
|
|
864
|
+
if (progressToken !== void 0)
|
|
865
|
+
this.progressRequests.set(progressToken, id);
|
|
866
|
+
options.signal?.addEventListener("abort", entry.onAbort, { once: true });
|
|
867
|
+
this.armTimeout(id, entry);
|
|
868
|
+
transport.send(message, { signal: entry.sendController.signal }).catch((error) => this.cancelPending(id, error, false));
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
async notifyInternal(method, params, allowConnecting) {
|
|
872
|
+
await this.requireTransport(allowConnecting).send({
|
|
873
|
+
jsonrpc: "2.0",
|
|
874
|
+
method,
|
|
875
|
+
...params === void 0 ? {} : { params }
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
requireTransport(allowConnecting) {
|
|
879
|
+
if (this.transport && (this.state === "connected" || allowConnecting && this.state === "connecting")) {
|
|
880
|
+
return this.transport;
|
|
881
|
+
}
|
|
882
|
+
throw new McpConnectionClosedError(`MCP client is ${this.state}`);
|
|
883
|
+
}
|
|
884
|
+
handleMessage(message) {
|
|
885
|
+
if (isJsonRpcResponse(message)) {
|
|
886
|
+
this.handleResponse(message);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (isJsonRpcRequest(message)) {
|
|
890
|
+
void this.handleRequest(message);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (isJsonRpcNotification(message)) {
|
|
894
|
+
this.handleNotification(message.method, message.params);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
this.emitError(new McpError(JSON_RPC_ERROR_CODES.invalidRequest, "Received invalid JSON-RPC message"));
|
|
898
|
+
}
|
|
899
|
+
handleResponse(message) {
|
|
900
|
+
const entry = this.pending.get(message.id);
|
|
901
|
+
if (!entry) {
|
|
902
|
+
this.emitError(new Error(`Received response for unknown MCP request ${String(message.id)}`));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
this.removePending(message.id, entry);
|
|
906
|
+
if ("error" in message)
|
|
907
|
+
entry.reject(new McpError(message.error.code, message.error.message, message.error.data));
|
|
908
|
+
else
|
|
909
|
+
entry.resolve(message.result);
|
|
910
|
+
}
|
|
911
|
+
async handleRequest(message) {
|
|
912
|
+
const transport = this.transport;
|
|
913
|
+
if (!transport)
|
|
914
|
+
return;
|
|
915
|
+
const handler = this.requestHandlers.get(message.method);
|
|
916
|
+
if (!handler) {
|
|
917
|
+
await transport.send({
|
|
918
|
+
jsonrpc: "2.0",
|
|
919
|
+
id: message.id,
|
|
920
|
+
error: { code: JSON_RPC_ERROR_CODES.methodNotFound, message: `Method not found: ${message.method}` }
|
|
921
|
+
}).catch((error) => this.emitError(error));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const controller = new AbortController();
|
|
925
|
+
this.incoming.set(message.id, controller);
|
|
926
|
+
try {
|
|
927
|
+
const result = await handler(message.params, { signal: controller.signal });
|
|
928
|
+
await transport.send({ jsonrpc: "2.0", id: message.id, result: result ?? {} });
|
|
929
|
+
} catch (error) {
|
|
930
|
+
const responseError = error instanceof McpError ? { code: error.code, message: error.message, data: error.data } : { code: JSON_RPC_ERROR_CODES.internalError, message: toError(error).message };
|
|
931
|
+
await transport.send({ jsonrpc: "2.0", id: message.id, error: responseError }).catch((sendError) => this.emitError(sendError));
|
|
932
|
+
} finally {
|
|
933
|
+
this.incoming.delete(message.id);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
handleNotification(method, params) {
|
|
937
|
+
if (method === "notifications/progress")
|
|
938
|
+
this.handleProgress(params);
|
|
939
|
+
else if (method === "notifications/cancelled")
|
|
940
|
+
this.handleCancelled(params);
|
|
941
|
+
for (const listener of this.notificationListeners.get(method) ?? []) {
|
|
942
|
+
try {
|
|
943
|
+
listener(params);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
this.emitError(error);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
handleProgress(params) {
|
|
950
|
+
if (!isObject(params) || !isJsonRpcId(params.progressToken) || typeof params.progress !== "number")
|
|
951
|
+
return;
|
|
952
|
+
const requestId = this.progressRequests.get(params.progressToken);
|
|
953
|
+
const entry = requestId === void 0 ? void 0 : this.pending.get(requestId);
|
|
954
|
+
if (requestId === void 0 || !entry)
|
|
955
|
+
return;
|
|
956
|
+
this.armTimeout(requestId, entry);
|
|
957
|
+
try {
|
|
958
|
+
entry.onProgress?.(params);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
this.emitError(error);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
handleCancelled(params) {
|
|
964
|
+
if (isObject(params) && isJsonRpcId(params.requestId))
|
|
965
|
+
this.incoming.get(params.requestId)?.abort(params.reason);
|
|
966
|
+
}
|
|
967
|
+
armTimeout(id, entry) {
|
|
968
|
+
if (entry.timer)
|
|
969
|
+
clearTimeout(entry.timer);
|
|
970
|
+
if (!Number.isFinite(entry.timeoutMs) || entry.timeoutMs <= 0)
|
|
971
|
+
return;
|
|
972
|
+
entry.timer = setTimeout(() => {
|
|
973
|
+
this.cancelPending(id, new McpTimeoutError(entry.timeoutMs), entry.cancellable, "Request timed out");
|
|
974
|
+
}, entry.timeoutMs);
|
|
975
|
+
}
|
|
976
|
+
cancelPending(id, error, notifyServer, reason) {
|
|
977
|
+
const entry = this.pending.get(id);
|
|
978
|
+
if (!entry)
|
|
979
|
+
return;
|
|
980
|
+
this.removePending(id, entry);
|
|
981
|
+
entry.reject(error);
|
|
982
|
+
if (notifyServer && this.transport) {
|
|
983
|
+
void this.transport.send({
|
|
984
|
+
jsonrpc: "2.0",
|
|
985
|
+
method: "notifications/cancelled",
|
|
986
|
+
params: { requestId: id, ...reason ? { reason } : {} }
|
|
987
|
+
}).catch((sendError) => this.emitError(sendError));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
removePending(id, entry) {
|
|
991
|
+
this.pending.delete(id);
|
|
992
|
+
if (entry.timer)
|
|
993
|
+
clearTimeout(entry.timer);
|
|
994
|
+
if (entry.progressToken !== void 0)
|
|
995
|
+
this.progressRequests.delete(entry.progressToken);
|
|
996
|
+
entry.signal?.removeEventListener("abort", entry.onAbort);
|
|
997
|
+
entry.sendController.abort();
|
|
998
|
+
}
|
|
999
|
+
rejectPending(error) {
|
|
1000
|
+
for (const [id, entry] of this.pending) {
|
|
1001
|
+
this.removePending(id, entry);
|
|
1002
|
+
entry.reject(error);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
handleTransportClose() {
|
|
1006
|
+
this.markClosed(new McpConnectionClosedError());
|
|
1007
|
+
}
|
|
1008
|
+
/** Idempotent: rejects in-flight requests, aborts server requests we are serving, and flips the state. */
|
|
1009
|
+
markClosed(error) {
|
|
1010
|
+
const wasClosed = this.state === "closed";
|
|
1011
|
+
this.state = "closed";
|
|
1012
|
+
this.rejectPending(error);
|
|
1013
|
+
for (const controller of this.incoming.values())
|
|
1014
|
+
controller.abort(error);
|
|
1015
|
+
this.incoming.clear();
|
|
1016
|
+
if (wasClosed)
|
|
1017
|
+
return;
|
|
1018
|
+
for (const listener of [...this.closeListeners]) {
|
|
1019
|
+
try {
|
|
1020
|
+
listener();
|
|
1021
|
+
} catch (listenerError) {
|
|
1022
|
+
this.emitError(listenerError);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
emitError(error) {
|
|
1027
|
+
const normalized = toError(error);
|
|
1028
|
+
for (const listener of this.errorListeners)
|
|
1029
|
+
listener(normalized);
|
|
1030
|
+
}
|
|
1031
|
+
disposeTransportListeners() {
|
|
1032
|
+
for (const dispose of this.disposers.splice(0))
|
|
1033
|
+
dispose();
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/stdio.js
|
|
1038
|
+
var import_cross_spawn = __toESM(require_cross_spawn(), 1);
|
|
1039
|
+
import { spawn } from "node:child_process";
|
|
1040
|
+
import process2 from "node:process";
|
|
1041
|
+
|
|
1042
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/transport.js
|
|
1043
|
+
var DEFAULT_MAX_MESSAGE_BYTES = 16 * 1024 * 1024;
|
|
1044
|
+
var TransportEvents = class {
|
|
1045
|
+
messageListeners = /* @__PURE__ */ new Set();
|
|
1046
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
1047
|
+
closeListeners = /* @__PURE__ */ new Set();
|
|
1048
|
+
closeEmitted = false;
|
|
1049
|
+
onMessage(listener) {
|
|
1050
|
+
this.messageListeners.add(listener);
|
|
1051
|
+
return () => this.messageListeners.delete(listener);
|
|
1052
|
+
}
|
|
1053
|
+
onError(listener) {
|
|
1054
|
+
this.errorListeners.add(listener);
|
|
1055
|
+
return () => this.errorListeners.delete(listener);
|
|
1056
|
+
}
|
|
1057
|
+
onClose(listener) {
|
|
1058
|
+
this.closeListeners.add(listener);
|
|
1059
|
+
return () => this.closeListeners.delete(listener);
|
|
1060
|
+
}
|
|
1061
|
+
emitMessage(message) {
|
|
1062
|
+
for (const listener of this.messageListeners)
|
|
1063
|
+
listener(message);
|
|
1064
|
+
}
|
|
1065
|
+
emitError(error) {
|
|
1066
|
+
const normalized = toError(error);
|
|
1067
|
+
for (const listener of this.errorListeners)
|
|
1068
|
+
listener(normalized);
|
|
1069
|
+
}
|
|
1070
|
+
emitClose() {
|
|
1071
|
+
if (this.closeEmitted)
|
|
1072
|
+
return;
|
|
1073
|
+
this.closeEmitted = true;
|
|
1074
|
+
for (const listener of this.closeListeners)
|
|
1075
|
+
listener();
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/stdio.js
|
|
1080
|
+
var DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
|
|
1081
|
+
var DEFAULT_CLOSE_TIMEOUT_MS = 2e3;
|
|
1082
|
+
var STDIN_CLOSE_GRACE_MS = 500;
|
|
1083
|
+
var USE_PROCESS_GROUPS = process2.platform !== "win32";
|
|
1084
|
+
var liveProcessGroups = /* @__PURE__ */ new Set();
|
|
1085
|
+
var exitHookInstalled = false;
|
|
1086
|
+
function killProcessTree(child, signal) {
|
|
1087
|
+
const pid = child.pid;
|
|
1088
|
+
if (process2.platform === "win32" && pid !== void 0) {
|
|
1089
|
+
if (child.exitCode !== null)
|
|
1090
|
+
return;
|
|
1091
|
+
spawn("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }).on("error", () => {
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (USE_PROCESS_GROUPS && pid !== void 0) {
|
|
1096
|
+
try {
|
|
1097
|
+
process2.kill(-pid, signal);
|
|
1098
|
+
return;
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
child.kill(signal);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function installExitHook() {
|
|
1108
|
+
if (exitHookInstalled)
|
|
1109
|
+
return;
|
|
1110
|
+
exitHookInstalled = true;
|
|
1111
|
+
process2.once("exit", () => {
|
|
1112
|
+
for (const pid of liveProcessGroups) {
|
|
1113
|
+
try {
|
|
1114
|
+
process2.kill(-pid, "SIGTERM");
|
|
1115
|
+
} catch {
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
var StdioTransport = class extends TransportEvents {
|
|
1121
|
+
options;
|
|
1122
|
+
child;
|
|
1123
|
+
stdoutBuffer = Buffer.alloc(0);
|
|
1124
|
+
stderrBuffer = Buffer.alloc(0);
|
|
1125
|
+
started = false;
|
|
1126
|
+
closed = false;
|
|
1127
|
+
constructor(options) {
|
|
1128
|
+
super();
|
|
1129
|
+
this.options = Object.freeze({ ...options, args: options.args ? [...options.args] : void 0 });
|
|
1130
|
+
}
|
|
1131
|
+
get pid() {
|
|
1132
|
+
return this.child?.pid;
|
|
1133
|
+
}
|
|
1134
|
+
get stderr() {
|
|
1135
|
+
return this.stderrBuffer.toString("utf8");
|
|
1136
|
+
}
|
|
1137
|
+
async start() {
|
|
1138
|
+
if (this.started)
|
|
1139
|
+
throw new Error("MCP stdio transport already started");
|
|
1140
|
+
if (this.closed)
|
|
1141
|
+
throw new McpConnectionClosedError();
|
|
1142
|
+
this.started = true;
|
|
1143
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env } : { ...process2.env, ...this.options.env };
|
|
1144
|
+
const child = (0, import_cross_spawn.default)(this.options.command, this.options.args ?? [], {
|
|
1145
|
+
cwd: this.options.cwd,
|
|
1146
|
+
env,
|
|
1147
|
+
stdio: ["pipe", "pipe", this.options.stderr === "inherit" ? "inherit" : "pipe"],
|
|
1148
|
+
windowsHide: true,
|
|
1149
|
+
// Own process group, so closing the transport can terminate the server's children too.
|
|
1150
|
+
detached: USE_PROCESS_GROUPS
|
|
1151
|
+
});
|
|
1152
|
+
this.child = child;
|
|
1153
|
+
const pid = child.pid;
|
|
1154
|
+
if (USE_PROCESS_GROUPS && pid !== void 0) {
|
|
1155
|
+
installExitHook();
|
|
1156
|
+
liveProcessGroups.add(pid);
|
|
1157
|
+
child.once("exit", () => liveProcessGroups.delete(pid));
|
|
1158
|
+
}
|
|
1159
|
+
child.stdout?.on("data", (chunk) => this.handleStdout(chunk));
|
|
1160
|
+
child.stdout?.on("error", (error) => this.emitError(error));
|
|
1161
|
+
child.stdin?.on("error", (error) => {
|
|
1162
|
+
if (!this.closed)
|
|
1163
|
+
this.emitError(error);
|
|
1164
|
+
});
|
|
1165
|
+
child.stderr?.on("data", (chunk) => this.handleStderr(chunk));
|
|
1166
|
+
child.stderr?.on("error", (error) => this.emitError(error));
|
|
1167
|
+
child.on("close", () => {
|
|
1168
|
+
this.child = void 0;
|
|
1169
|
+
if (this.stdoutBuffer.toString("utf8").trim()) {
|
|
1170
|
+
this.emitError(new Error("MCP stdio server closed with an incomplete JSON-RPC message"));
|
|
1171
|
+
}
|
|
1172
|
+
this.stdoutBuffer = Buffer.alloc(0);
|
|
1173
|
+
this.emitClose();
|
|
1174
|
+
});
|
|
1175
|
+
await new Promise((resolve, reject) => {
|
|
1176
|
+
const onSpawn = () => {
|
|
1177
|
+
child.off("error", onError);
|
|
1178
|
+
resolve();
|
|
1179
|
+
};
|
|
1180
|
+
const onError = (error) => {
|
|
1181
|
+
child.off("spawn", onSpawn);
|
|
1182
|
+
reject(error);
|
|
1183
|
+
};
|
|
1184
|
+
child.once("spawn", onSpawn);
|
|
1185
|
+
child.once("error", onError);
|
|
1186
|
+
});
|
|
1187
|
+
child.on("error", (error) => {
|
|
1188
|
+
if (!this.closed)
|
|
1189
|
+
this.emitError(error);
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
async send(message) {
|
|
1193
|
+
const stdin = this.child?.stdin;
|
|
1194
|
+
if (!this.started || this.closed || !stdin?.writable)
|
|
1195
|
+
throw new McpConnectionClosedError();
|
|
1196
|
+
const payload = `${JSON.stringify(message)}
|
|
1197
|
+
`;
|
|
1198
|
+
await new Promise((resolve, reject) => {
|
|
1199
|
+
stdin.write(payload, (error) => error ? reject(error) : resolve());
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
async close() {
|
|
1203
|
+
if (this.closed)
|
|
1204
|
+
return;
|
|
1205
|
+
this.closed = true;
|
|
1206
|
+
const child = this.child;
|
|
1207
|
+
if (!child) {
|
|
1208
|
+
this.emitClose();
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1212
|
+
child.stdin?.end();
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const closeTimeoutMs = this.options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS;
|
|
1216
|
+
await new Promise((resolve) => {
|
|
1217
|
+
const timers = [];
|
|
1218
|
+
child.once("close", () => {
|
|
1219
|
+
for (const timer of timers)
|
|
1220
|
+
clearTimeout(timer);
|
|
1221
|
+
killProcessTree(child, "SIGTERM");
|
|
1222
|
+
resolve();
|
|
1223
|
+
});
|
|
1224
|
+
const grace = Math.min(STDIN_CLOSE_GRACE_MS, closeTimeoutMs);
|
|
1225
|
+
timers.push(setTimeout(() => killProcessTree(child, "SIGTERM"), grace));
|
|
1226
|
+
timers.push(setTimeout(() => killProcessTree(child, "SIGKILL"), grace + closeTimeoutMs));
|
|
1227
|
+
child.stdin?.end();
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
handleStdout(chunk) {
|
|
1231
|
+
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
|
1232
|
+
const maxMessageBytes = this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
1233
|
+
while (true) {
|
|
1234
|
+
const newline = this.stdoutBuffer.indexOf(10);
|
|
1235
|
+
if (newline < 0) {
|
|
1236
|
+
if (this.stdoutBuffer.length > maxMessageBytes) {
|
|
1237
|
+
this.stdoutBuffer = Buffer.alloc(0);
|
|
1238
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxMessageBytes} bytes`));
|
|
1239
|
+
}
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const line = this.stdoutBuffer.subarray(0, newline);
|
|
1243
|
+
this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1);
|
|
1244
|
+
if (line.length > maxMessageBytes) {
|
|
1245
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxMessageBytes} bytes`));
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
const text = line.toString("utf8").replace(/\r$/, "");
|
|
1249
|
+
if (!text.trim())
|
|
1250
|
+
continue;
|
|
1251
|
+
try {
|
|
1252
|
+
this.emitMessage(parseJsonRpcMessage(JSON.parse(text)));
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
this.emitError(error);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
handleStderr(chunk) {
|
|
1259
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1260
|
+
const maxStderrBytes = this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES;
|
|
1261
|
+
this.stderrBuffer = Buffer.concat([this.stderrBuffer, buffer]);
|
|
1262
|
+
if (this.stderrBuffer.length > maxStderrBytes)
|
|
1263
|
+
this.stderrBuffer = this.stderrBuffer.subarray(-maxStderrBytes);
|
|
1264
|
+
this.options.onStderr?.(buffer.toString("utf8"));
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
// node_modules/@earendil-works/pi-mcp/dist/transports/streamable-http.js
|
|
1269
|
+
var MAX_ERROR_BODY_BYTES = 8 * 1024;
|
|
1270
|
+
var ERROR_MESSAGE_BODY_CHARS = 500;
|
|
1271
|
+
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 1e3;
|
|
1272
|
+
var DEFAULT_RECONNECT_MAX_DELAY_MS = 3e4;
|
|
1273
|
+
var DEFAULT_RECONNECT_MAX_RETRIES = 5;
|
|
1274
|
+
async function consumeSseStream(stream, options) {
|
|
1275
|
+
const reader = stream.getReader();
|
|
1276
|
+
const decoder = new TextDecoder();
|
|
1277
|
+
let buffered = "";
|
|
1278
|
+
let eventName;
|
|
1279
|
+
let eventId;
|
|
1280
|
+
let dataLines = [];
|
|
1281
|
+
let dataBytes = 0;
|
|
1282
|
+
const maxEventBytes = options.maxEventBytes ?? DEFAULT_MAX_MESSAGE_BYTES;
|
|
1283
|
+
const dispatch = () => {
|
|
1284
|
+
if (dataLines.length === 0) {
|
|
1285
|
+
eventName = void 0;
|
|
1286
|
+
eventId = void 0;
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
const data = dataLines.join("\n");
|
|
1290
|
+
options.onEvent({ ...eventName ? { event: eventName } : {}, data, ...eventId ? { id: eventId } : {} });
|
|
1291
|
+
eventName = void 0;
|
|
1292
|
+
eventId = void 0;
|
|
1293
|
+
dataLines = [];
|
|
1294
|
+
dataBytes = 0;
|
|
1295
|
+
};
|
|
1296
|
+
const processLine = (rawLine) => {
|
|
1297
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
1298
|
+
if (line === "") {
|
|
1299
|
+
dispatch();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (line.startsWith(":"))
|
|
1303
|
+
return;
|
|
1304
|
+
const colon = line.indexOf(":");
|
|
1305
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
1306
|
+
let value = colon < 0 ? "" : line.slice(colon + 1);
|
|
1307
|
+
if (value.startsWith(" "))
|
|
1308
|
+
value = value.slice(1);
|
|
1309
|
+
if (field === "data") {
|
|
1310
|
+
dataBytes += Buffer.byteLength(value) + (dataLines.length > 0 ? 1 : 0);
|
|
1311
|
+
if (dataBytes > maxEventBytes)
|
|
1312
|
+
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
1313
|
+
dataLines.push(value);
|
|
1314
|
+
} else if (field === "event")
|
|
1315
|
+
eventName = value;
|
|
1316
|
+
else if (field === "id" && !value.includes("\0")) {
|
|
1317
|
+
eventId = value;
|
|
1318
|
+
options.onId?.(value);
|
|
1319
|
+
} else if (field === "retry" && /^\d+$/.test(value))
|
|
1320
|
+
options.onRetry?.(Number(value));
|
|
1321
|
+
};
|
|
1322
|
+
try {
|
|
1323
|
+
while (true) {
|
|
1324
|
+
const { done, value } = await reader.read();
|
|
1325
|
+
if (done)
|
|
1326
|
+
break;
|
|
1327
|
+
buffered += decoder.decode(value, { stream: true });
|
|
1328
|
+
let newline = buffered.indexOf("\n");
|
|
1329
|
+
while (newline >= 0) {
|
|
1330
|
+
processLine(buffered.slice(0, newline));
|
|
1331
|
+
buffered = buffered.slice(newline + 1);
|
|
1332
|
+
newline = buffered.indexOf("\n");
|
|
1333
|
+
}
|
|
1334
|
+
if (Buffer.byteLength(buffered) > maxEventBytes)
|
|
1335
|
+
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
1336
|
+
}
|
|
1337
|
+
buffered += decoder.decode();
|
|
1338
|
+
if (buffered)
|
|
1339
|
+
processLine(buffered);
|
|
1340
|
+
dispatch();
|
|
1341
|
+
} finally {
|
|
1342
|
+
reader.releaseLock();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
var McpHttpError = class extends Error {
|
|
1346
|
+
status;
|
|
1347
|
+
body;
|
|
1348
|
+
constructor(status, message, body = "") {
|
|
1349
|
+
super(message);
|
|
1350
|
+
this.name = "McpHttpError";
|
|
1351
|
+
this.status = status;
|
|
1352
|
+
this.body = body;
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
var McpAuthRequiredError = class extends McpHttpError {
|
|
1356
|
+
wwwAuthenticate;
|
|
1357
|
+
constructor(response, body = "") {
|
|
1358
|
+
super(401, "MCP server requires authentication", body);
|
|
1359
|
+
this.name = "McpAuthRequiredError";
|
|
1360
|
+
this.wwwAuthenticate = response.headers.get("www-authenticate");
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
var McpSessionExpiredError = class extends McpHttpError {
|
|
1364
|
+
constructor(body = "") {
|
|
1365
|
+
super(404, "MCP session expired", body);
|
|
1366
|
+
this.name = "McpSessionExpiredError";
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
function contentType(response) {
|
|
1370
|
+
return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1371
|
+
}
|
|
1372
|
+
function needsAuthorization(response) {
|
|
1373
|
+
if (response.status === 401)
|
|
1374
|
+
return true;
|
|
1375
|
+
if (response.status !== 403)
|
|
1376
|
+
return false;
|
|
1377
|
+
return /(?:^|[\s,])error="?insufficient_scope"?/i.test(response.headers.get("www-authenticate") ?? "");
|
|
1378
|
+
}
|
|
1379
|
+
function isTransientStatus(status) {
|
|
1380
|
+
return status === 408 || status === 429 || status >= 500;
|
|
1381
|
+
}
|
|
1382
|
+
function discard(response) {
|
|
1383
|
+
return response.body?.cancel().catch(() => {
|
|
1384
|
+
}) ?? Promise.resolve();
|
|
1385
|
+
}
|
|
1386
|
+
function describeHttpFailure(status, body) {
|
|
1387
|
+
const text = body.trim();
|
|
1388
|
+
const snippet = text.length > ERROR_MESSAGE_BODY_CHARS ? `${text.slice(0, ERROR_MESSAGE_BODY_CHARS - 3)}...` : text;
|
|
1389
|
+
return `MCP HTTP request failed with status ${status}${snippet ? `: ${snippet}` : ""}`;
|
|
1390
|
+
}
|
|
1391
|
+
var StreamableHttpTransport = class extends TransportEvents {
|
|
1392
|
+
url;
|
|
1393
|
+
options;
|
|
1394
|
+
fetch;
|
|
1395
|
+
controller = new AbortController();
|
|
1396
|
+
started = false;
|
|
1397
|
+
closed = false;
|
|
1398
|
+
sessionIdValue;
|
|
1399
|
+
protocolVersion;
|
|
1400
|
+
getStreamStarted = false;
|
|
1401
|
+
/** Access token of the latest request, which `close()` reuses instead of asking the auth provider. */
|
|
1402
|
+
lastToken;
|
|
1403
|
+
constructor(options) {
|
|
1404
|
+
super();
|
|
1405
|
+
this.options = Object.freeze({ ...options, headers: options.headers ? { ...options.headers } : void 0 });
|
|
1406
|
+
this.url = new URL(options.url);
|
|
1407
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
1408
|
+
this.fetch = (input, init) => fetch(input, init);
|
|
1409
|
+
}
|
|
1410
|
+
get sessionId() {
|
|
1411
|
+
return this.sessionIdValue;
|
|
1412
|
+
}
|
|
1413
|
+
async start() {
|
|
1414
|
+
if (this.started)
|
|
1415
|
+
throw new Error("MCP Streamable HTTP transport already started");
|
|
1416
|
+
if (this.closed)
|
|
1417
|
+
throw new McpConnectionClosedError();
|
|
1418
|
+
this.started = true;
|
|
1419
|
+
}
|
|
1420
|
+
setProtocolVersion(version) {
|
|
1421
|
+
this.protocolVersion = version;
|
|
1422
|
+
}
|
|
1423
|
+
async send(message, { signal } = {}) {
|
|
1424
|
+
if (!this.started || this.closed)
|
|
1425
|
+
throw new McpConnectionClosedError();
|
|
1426
|
+
const response = await this.authorizedFetch("POST", {
|
|
1427
|
+
headers: { accept: "application/json, text/event-stream", "content-type": "application/json" },
|
|
1428
|
+
body: JSON.stringify(message)
|
|
1429
|
+
}, signal);
|
|
1430
|
+
await this.checkResponse(response);
|
|
1431
|
+
this.captureSession(response);
|
|
1432
|
+
if (!isJsonRpcRequest(message)) {
|
|
1433
|
+
await discard(response);
|
|
1434
|
+
if ("method" in message && message.method === "notifications/initialized")
|
|
1435
|
+
this.startGetStream();
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (response.status === 202 || response.status === 204) {
|
|
1439
|
+
throw new McpHttpError(response.status, `MCP server accepted request ${message.method} without a response`);
|
|
1440
|
+
}
|
|
1441
|
+
const type = contentType(response);
|
|
1442
|
+
if (type === "application/json") {
|
|
1443
|
+
const body = await response.json();
|
|
1444
|
+
for (const item of Array.isArray(body) ? body : [body])
|
|
1445
|
+
this.emitMessage(parseJsonRpcMessage(item));
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (type === "text/event-stream" && response.body) {
|
|
1449
|
+
void this.consumeResponseStream(response.body, message.id);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
await discard(response);
|
|
1453
|
+
throw new McpHttpError(response.status, `Unsupported MCP response content type: ${type ?? "missing"}`);
|
|
1454
|
+
}
|
|
1455
|
+
async close() {
|
|
1456
|
+
if (this.closed)
|
|
1457
|
+
return;
|
|
1458
|
+
this.closed = true;
|
|
1459
|
+
this.controller.abort();
|
|
1460
|
+
if (this.started && this.sessionIdValue) {
|
|
1461
|
+
try {
|
|
1462
|
+
const response = await this.fetch(this.url, {
|
|
1463
|
+
method: "DELETE",
|
|
1464
|
+
headers: this.buildHeaders({}, this.lastToken),
|
|
1465
|
+
signal: AbortSignal.timeout(1e3)
|
|
1466
|
+
});
|
|
1467
|
+
await discard(response);
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
this.emitClose();
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Fetch with auth headers. A 401 (or a 403 asking for more scope) is handed to the auth provider
|
|
1475
|
+
* once, and the request is retried with whatever credentials it left behind.
|
|
1476
|
+
*
|
|
1477
|
+
* `signal` only stops dispatch: getting a token or the authorization recovery can take a while, and a
|
|
1478
|
+
* request its sender gave up on in the meantime must not reach the server. It does not abort the shared
|
|
1479
|
+
* recovery, which other requests may wait for.
|
|
1480
|
+
*/
|
|
1481
|
+
async authorizedFetch(method, init, signal) {
|
|
1482
|
+
const onUnauthorized = this.options.authProvider?.onUnauthorized?.bind(this.options.authProvider);
|
|
1483
|
+
for (let attempt = 0; ; attempt++) {
|
|
1484
|
+
const { headers, token } = await this.headers(init.headers);
|
|
1485
|
+
if (signal?.aborted)
|
|
1486
|
+
throw new McpAbortError();
|
|
1487
|
+
const response = await this.fetch(this.url, {
|
|
1488
|
+
method,
|
|
1489
|
+
headers,
|
|
1490
|
+
body: init.body,
|
|
1491
|
+
signal: this.controller.signal
|
|
1492
|
+
});
|
|
1493
|
+
if (attempt > 0 || !onUnauthorized || !needsAuthorization(response))
|
|
1494
|
+
return response;
|
|
1495
|
+
try {
|
|
1496
|
+
await onUnauthorized({ response, serverUrl: this.url, fetch: this.fetch, token });
|
|
1497
|
+
} finally {
|
|
1498
|
+
await discard(response);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async headers(extra = {}) {
|
|
1503
|
+
const token = await this.options.authProvider?.token();
|
|
1504
|
+
this.lastToken = token;
|
|
1505
|
+
return { headers: this.buildHeaders(extra, token), ...token ? { token } : {} };
|
|
1506
|
+
}
|
|
1507
|
+
buildHeaders(extra, token) {
|
|
1508
|
+
const headers = new Headers(this.options.headers);
|
|
1509
|
+
for (const [name, value] of Object.entries(extra))
|
|
1510
|
+
headers.set(name, value);
|
|
1511
|
+
if (this.sessionIdValue)
|
|
1512
|
+
headers.set("Mcp-Session-Id", this.sessionIdValue);
|
|
1513
|
+
if (this.protocolVersion)
|
|
1514
|
+
headers.set("MCP-Protocol-Version", this.protocolVersion);
|
|
1515
|
+
if (token)
|
|
1516
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
1517
|
+
return headers;
|
|
1518
|
+
}
|
|
1519
|
+
captureSession(response) {
|
|
1520
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
1521
|
+
if (sessionId)
|
|
1522
|
+
this.sessionIdValue = sessionId;
|
|
1523
|
+
}
|
|
1524
|
+
async checkResponse(response) {
|
|
1525
|
+
if (response.ok)
|
|
1526
|
+
return;
|
|
1527
|
+
const body = (await response.text().catch(() => "")).slice(0, MAX_ERROR_BODY_BYTES);
|
|
1528
|
+
if (response.status === 401)
|
|
1529
|
+
throw new McpAuthRequiredError(response, body);
|
|
1530
|
+
if (response.status === 404 && this.sessionIdValue)
|
|
1531
|
+
throw new McpSessionExpiredError(body);
|
|
1532
|
+
throw new McpHttpError(response.status, describeHttpFailure(response.status, body), body);
|
|
1533
|
+
}
|
|
1534
|
+
async consumeSse(stream, cursor, onMessage) {
|
|
1535
|
+
await consumeSseStream(stream, {
|
|
1536
|
+
maxEventBytes: this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES,
|
|
1537
|
+
onId: (id) => {
|
|
1538
|
+
cursor.lastEventId = id;
|
|
1539
|
+
},
|
|
1540
|
+
onRetry: (delayMs) => {
|
|
1541
|
+
cursor.retryMs = delayMs;
|
|
1542
|
+
},
|
|
1543
|
+
onEvent: (event) => {
|
|
1544
|
+
cursor.received = true;
|
|
1545
|
+
if (!event.data.trim() || event.event !== void 0 && event.event !== "message")
|
|
1546
|
+
return;
|
|
1547
|
+
let message;
|
|
1548
|
+
try {
|
|
1549
|
+
message = parseJsonRpcMessage(JSON.parse(event.data));
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
this.emitError(error);
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
onMessage?.(message);
|
|
1555
|
+
this.emitMessage(message);
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Read the SSE stream answering one request. When the stream ends or breaks before the response
|
|
1561
|
+
* arrives and the server assigned event IDs, resume it with GET and `Last-Event-ID`, as the
|
|
1562
|
+
* server may close response streams at will. Otherwise only this request fails.
|
|
1563
|
+
*/
|
|
1564
|
+
async consumeResponseStream(body, requestId) {
|
|
1565
|
+
const cursor = { lastEventId: void 0, retryMs: void 0, received: false };
|
|
1566
|
+
let answered = false;
|
|
1567
|
+
const onMessage = (message) => {
|
|
1568
|
+
if (isJsonRpcResponse(message) && message.id === requestId)
|
|
1569
|
+
answered = true;
|
|
1570
|
+
};
|
|
1571
|
+
let stream = body;
|
|
1572
|
+
let failure;
|
|
1573
|
+
for (let attempt = 0; ; ) {
|
|
1574
|
+
if (stream) {
|
|
1575
|
+
try {
|
|
1576
|
+
await this.consumeSse(stream, cursor, onMessage);
|
|
1577
|
+
failure = void 0;
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
failure = error;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
if (answered || this.closed)
|
|
1583
|
+
return;
|
|
1584
|
+
if (failure !== void 0 && !this.isRetryable(failure))
|
|
1585
|
+
break;
|
|
1586
|
+
if (cursor.lastEventId === void 0 || attempt >= this.maxRetries())
|
|
1587
|
+
break;
|
|
1588
|
+
if (cursor.received)
|
|
1589
|
+
attempt = 0;
|
|
1590
|
+
cursor.received = false;
|
|
1591
|
+
if (!await this.sleep(this.reconnectDelay(attempt++, cursor.retryMs)))
|
|
1592
|
+
return;
|
|
1593
|
+
try {
|
|
1594
|
+
stream = await this.openSseStream(cursor.lastEventId);
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
failure = error;
|
|
1597
|
+
if (!this.isRetryable(error))
|
|
1598
|
+
break;
|
|
1599
|
+
stream = void 0;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
if (this.closed)
|
|
1603
|
+
return;
|
|
1604
|
+
const reason = failure === void 0 ? "stream ended without a response" : toError(failure).message;
|
|
1605
|
+
this.emitMessage({
|
|
1606
|
+
jsonrpc: "2.0",
|
|
1607
|
+
id: requestId,
|
|
1608
|
+
error: { code: JSON_RPC_ERROR_CODES.internalError, message: `MCP response stream failed: ${reason}` }
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
startGetStream() {
|
|
1612
|
+
if (this.options.openGetStream === false || this.getStreamStarted || this.closed)
|
|
1613
|
+
return;
|
|
1614
|
+
this.getStreamStarted = true;
|
|
1615
|
+
void this.runGetStream();
|
|
1616
|
+
}
|
|
1617
|
+
/** Keep the server-to-client stream open, reconnecting with backoff when it drops. */
|
|
1618
|
+
async runGetStream() {
|
|
1619
|
+
const cursor = { lastEventId: void 0, retryMs: void 0, received: false };
|
|
1620
|
+
for (let attempt = 0; !this.closed; ) {
|
|
1621
|
+
try {
|
|
1622
|
+
const stream = await this.openSseStream(cursor.lastEventId);
|
|
1623
|
+
if (!stream)
|
|
1624
|
+
return;
|
|
1625
|
+
const openedAt = Date.now();
|
|
1626
|
+
await this.consumeSse(stream, cursor);
|
|
1627
|
+
if (cursor.received || Date.now() - openedAt > this.maxDelay())
|
|
1628
|
+
attempt = 0;
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
if (this.closed)
|
|
1631
|
+
return;
|
|
1632
|
+
if (!this.isRetryable(error)) {
|
|
1633
|
+
this.emitError(error);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
cursor.received = false;
|
|
1638
|
+
if (attempt >= this.maxRetries()) {
|
|
1639
|
+
this.emitError(new Error("MCP server-to-client stream dropped and could not be reopened"));
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (!await this.sleep(this.reconnectDelay(attempt++, cursor.retryMs)))
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/** Open a GET SSE stream. Resolves to undefined when the server answers 405 (no GET stream). */
|
|
1647
|
+
async openSseStream(lastEventId) {
|
|
1648
|
+
const response = await this.authorizedFetch("GET", {
|
|
1649
|
+
headers: {
|
|
1650
|
+
accept: "text/event-stream",
|
|
1651
|
+
...lastEventId === void 0 ? {} : { "last-event-id": lastEventId }
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
if (response.status === 405) {
|
|
1655
|
+
await discard(response);
|
|
1656
|
+
return void 0;
|
|
1657
|
+
}
|
|
1658
|
+
await this.checkResponse(response);
|
|
1659
|
+
this.captureSession(response);
|
|
1660
|
+
const type = contentType(response);
|
|
1661
|
+
if (type !== "text/event-stream" || !response.body) {
|
|
1662
|
+
await discard(response);
|
|
1663
|
+
throw new McpHttpError(response.status, `Unsupported MCP GET response content type: ${type ?? "missing"}`);
|
|
1664
|
+
}
|
|
1665
|
+
return response.body;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Network failures and transient statuses are retried; auth, session, and protocol errors are not.
|
|
1669
|
+
* `fetch` reports network failures, including a connection dropped mid-body, as `TypeError`.
|
|
1670
|
+
*/
|
|
1671
|
+
isRetryable(error) {
|
|
1672
|
+
if (error instanceof McpHttpError)
|
|
1673
|
+
return isTransientStatus(error.status);
|
|
1674
|
+
if (error instanceof TypeError)
|
|
1675
|
+
return true;
|
|
1676
|
+
const code2 = error?.code;
|
|
1677
|
+
return typeof code2 === "string" && (code2.startsWith("E") || code2.startsWith("UND_ERR"));
|
|
1678
|
+
}
|
|
1679
|
+
reconnectDelay(attempt, serverDelayMs) {
|
|
1680
|
+
if (serverDelayMs !== void 0)
|
|
1681
|
+
return serverDelayMs;
|
|
1682
|
+
const initial = this.options.reconnect?.initialDelayMs ?? DEFAULT_RECONNECT_INITIAL_DELAY_MS;
|
|
1683
|
+
return Math.min(initial * 2 ** attempt, this.maxDelay());
|
|
1684
|
+
}
|
|
1685
|
+
maxDelay() {
|
|
1686
|
+
return this.options.reconnect?.maxDelayMs ?? DEFAULT_RECONNECT_MAX_DELAY_MS;
|
|
1687
|
+
}
|
|
1688
|
+
maxRetries() {
|
|
1689
|
+
return this.options.reconnect?.maxRetries ?? DEFAULT_RECONNECT_MAX_RETRIES;
|
|
1690
|
+
}
|
|
1691
|
+
/** Resolves false when the transport closed while waiting. */
|
|
1692
|
+
sleep(ms) {
|
|
1693
|
+
const signal = this.controller.signal;
|
|
1694
|
+
if (signal.aborted)
|
|
1695
|
+
return Promise.resolve(false);
|
|
1696
|
+
return new Promise((resolve) => {
|
|
1697
|
+
const onAbort = () => {
|
|
1698
|
+
clearTimeout(timer);
|
|
1699
|
+
resolve(false);
|
|
1700
|
+
};
|
|
1701
|
+
const timer = setTimeout(() => {
|
|
1702
|
+
signal.removeEventListener("abort", onAbort);
|
|
1703
|
+
resolve(true);
|
|
1704
|
+
}, ms);
|
|
1705
|
+
timer.unref?.();
|
|
1706
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
// src/client/pi-connection.ts
|
|
1712
|
+
async function createPiConnection(options) {
|
|
1713
|
+
const clientOptions = {
|
|
1714
|
+
name: "mcpcat",
|
|
1715
|
+
version: options.version ?? "0.0.0"
|
|
1716
|
+
};
|
|
1717
|
+
const client = new McpClient(clientOptions);
|
|
1718
|
+
let transport;
|
|
1719
|
+
if (options.url) {
|
|
1720
|
+
transport = new StreamableHttpTransport({
|
|
1721
|
+
url: options.url,
|
|
1722
|
+
headers: options.headers
|
|
1723
|
+
});
|
|
1724
|
+
} else if (options.command) {
|
|
1725
|
+
transport = new StdioTransport({
|
|
1726
|
+
command: options.command,
|
|
1727
|
+
args: options.args,
|
|
1728
|
+
env: options.env,
|
|
1729
|
+
cwd: options.cwd,
|
|
1730
|
+
inheritEnv: true,
|
|
1731
|
+
stderr: "pipe"
|
|
1732
|
+
});
|
|
1733
|
+
} else {
|
|
1734
|
+
throw new Error("PiConnection requires either a url or a command");
|
|
1735
|
+
}
|
|
1736
|
+
const initResult = await client.connect(transport);
|
|
1737
|
+
return {
|
|
1738
|
+
async info() {
|
|
1739
|
+
return {
|
|
1740
|
+
name: initResult.serverInfo?.name,
|
|
1741
|
+
version: initResult.serverInfo?.version,
|
|
1742
|
+
title: initResult.serverInfo?.title,
|
|
1743
|
+
protocolVersion: initResult.protocolVersion,
|
|
1744
|
+
capabilities: initResult.capabilities,
|
|
1745
|
+
instructions: initResult.instructions
|
|
1746
|
+
};
|
|
1747
|
+
},
|
|
1748
|
+
async listTools() {
|
|
1749
|
+
return client.listTools();
|
|
1750
|
+
},
|
|
1751
|
+
async callTool(name, args2) {
|
|
1752
|
+
return client.callTool(name, args2);
|
|
1753
|
+
},
|
|
1754
|
+
async close() {
|
|
1755
|
+
await client.close();
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/client/stateless-connection.ts
|
|
1761
|
+
var STATELESS_PROTOCOL_VERSION = "2026-07-28";
|
|
1762
|
+
var StatelessRpcError = class extends Error {
|
|
1763
|
+
code;
|
|
1764
|
+
data;
|
|
1765
|
+
constructor(code2, message, data) {
|
|
1766
|
+
super(`MCP RPC error ${code2}: ${message}`);
|
|
1767
|
+
this.name = "StatelessRpcError";
|
|
1768
|
+
this.code = code2;
|
|
1769
|
+
this.data = data;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
var StatelessHttpError = class extends Error {
|
|
1773
|
+
status;
|
|
1774
|
+
constructor(status, statusText, method, snippet) {
|
|
1775
|
+
super(`MCP HTTP ${status} ${statusText} for ${method}: ${snippet || "(empty body)"}`);
|
|
1776
|
+
this.name = "StatelessHttpError";
|
|
1777
|
+
this.status = status;
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
var InputRequiredError = class extends Error {
|
|
1781
|
+
requestJson;
|
|
1782
|
+
constructor(requestJson) {
|
|
1783
|
+
super("Server requires input (resultType: input_required)");
|
|
1784
|
+
this.name = "InputRequiredError";
|
|
1785
|
+
this.requestJson = requestJson;
|
|
1786
|
+
}
|
|
1787
|
+
};
|
|
1788
|
+
function selectSseFrame(body, targetId) {
|
|
1789
|
+
const text = new TextDecoder().decode(body);
|
|
1790
|
+
const blocks = text.replace(/\r\n/g, "\n").split(/\n\n/);
|
|
1791
|
+
for (const block of blocks) {
|
|
1792
|
+
if (!block.trim()) continue;
|
|
1793
|
+
const dataLines = [];
|
|
1794
|
+
for (const line of block.split("\n")) {
|
|
1795
|
+
if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
|
|
1796
|
+
}
|
|
1797
|
+
if (dataLines.length === 0) continue;
|
|
1798
|
+
try {
|
|
1799
|
+
const parsed = JSON.parse(dataLines.join("\n"));
|
|
1800
|
+
if (parsed.id === targetId) return parsed;
|
|
1801
|
+
} catch {
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
throw new Error(`SSE stream ended without response for id ${targetId}`);
|
|
1805
|
+
}
|
|
1806
|
+
function mcpNameForMethod(method, params) {
|
|
1807
|
+
if (method === "tools/call") return params.name;
|
|
1808
|
+
if (method === "prompts/get") return params.name;
|
|
1809
|
+
if (method === "resources/read") return params.uri;
|
|
1810
|
+
return void 0;
|
|
1811
|
+
}
|
|
1812
|
+
async function createStatelessConnection(options) {
|
|
1813
|
+
const {
|
|
1814
|
+
url,
|
|
1815
|
+
headers: extraHeaders,
|
|
1816
|
+
fetchImpl,
|
|
1817
|
+
timeoutMs = 6e4,
|
|
1818
|
+
clientInfo = { name: "mcpcat", version: "0.0.0" }
|
|
1819
|
+
} = options;
|
|
1820
|
+
let nextId = 1;
|
|
1821
|
+
let negotiatedVersion;
|
|
1822
|
+
async function request(method, params) {
|
|
1823
|
+
const id = nextId++;
|
|
1824
|
+
const headers = {
|
|
1825
|
+
"Content-Type": "application/json",
|
|
1826
|
+
Accept: "application/json, text/event-stream",
|
|
1827
|
+
"Mcp-Method": method,
|
|
1828
|
+
...extraHeaders
|
|
1829
|
+
};
|
|
1830
|
+
const mcpName = mcpNameForMethod(method, params);
|
|
1831
|
+
if (mcpName) {
|
|
1832
|
+
headers["Mcp-Name"] = mcpName;
|
|
1833
|
+
}
|
|
1834
|
+
if (negotiatedVersion) {
|
|
1835
|
+
headers["Mcp-Protocol-Version"] = negotiatedVersion;
|
|
1836
|
+
}
|
|
1837
|
+
const meta = {
|
|
1838
|
+
protocolVersion: negotiatedVersion ?? STATELESS_PROTOCOL_VERSION,
|
|
1839
|
+
clientInfo
|
|
1840
|
+
};
|
|
1841
|
+
const body = JSON.stringify({
|
|
1842
|
+
jsonrpc: "2.0",
|
|
1843
|
+
id,
|
|
1844
|
+
method,
|
|
1845
|
+
params: { ...params, _meta: meta }
|
|
1846
|
+
});
|
|
1847
|
+
const controller = new AbortController();
|
|
1848
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1849
|
+
let res;
|
|
1850
|
+
try {
|
|
1851
|
+
res = await fetchImpl(url, {
|
|
1852
|
+
method: "POST",
|
|
1853
|
+
headers,
|
|
1854
|
+
body,
|
|
1855
|
+
signal: controller.signal
|
|
1856
|
+
});
|
|
1857
|
+
} finally {
|
|
1858
|
+
clearTimeout(timer);
|
|
1859
|
+
}
|
|
1860
|
+
if (res.status === 401) {
|
|
1861
|
+
throw new StatelessHttpError(401, "Unauthorized", method, "");
|
|
1862
|
+
}
|
|
1863
|
+
if (res.status >= 400) {
|
|
1864
|
+
const text = await res.text();
|
|
1865
|
+
let rpcError;
|
|
1866
|
+
try {
|
|
1867
|
+
const parsed = JSON.parse(text);
|
|
1868
|
+
if (parsed.error && typeof parsed.error.code === "number") {
|
|
1869
|
+
rpcError = parsed.error;
|
|
1870
|
+
}
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1873
|
+
if (rpcError) {
|
|
1874
|
+
throw new StatelessRpcError(rpcError.code, rpcError.message, rpcError.data);
|
|
1875
|
+
}
|
|
1876
|
+
throw new StatelessHttpError(res.status, res.statusText, method, text.slice(0, 512));
|
|
1877
|
+
}
|
|
1878
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
1879
|
+
let frame;
|
|
1880
|
+
if (ct.toLowerCase().includes("text/event-stream")) {
|
|
1881
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1882
|
+
frame = selectSseFrame(bytes, id);
|
|
1883
|
+
} else {
|
|
1884
|
+
frame = await res.json();
|
|
1885
|
+
}
|
|
1886
|
+
if (frame.error) {
|
|
1887
|
+
throw new StatelessRpcError(frame.error.code, frame.error.message, frame.error.data);
|
|
1888
|
+
}
|
|
1889
|
+
return frame.result;
|
|
1890
|
+
}
|
|
1891
|
+
const discoverResult = await request("server/discover", {
|
|
1892
|
+
protocolVersion: STATELESS_PROTOCOL_VERSION
|
|
1893
|
+
});
|
|
1894
|
+
const supported = Array.isArray(discoverResult?.supportedVersions) ? discoverResult.supportedVersions.filter((v) => typeof v === "string") : [];
|
|
1895
|
+
if (!supported.includes(STATELESS_PROTOCOL_VERSION)) {
|
|
1896
|
+
throw new Error(
|
|
1897
|
+
`Server does not support ${STATELESS_PROTOCOL_VERSION} (advertised: ${supported.join(", ") || "none"})`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
negotiatedVersion = STATELESS_PROTOCOL_VERSION;
|
|
1901
|
+
let serverInfoCache;
|
|
1902
|
+
return {
|
|
1903
|
+
async info() {
|
|
1904
|
+
if (serverInfoCache) return serverInfoCache;
|
|
1905
|
+
const result = discoverResult;
|
|
1906
|
+
serverInfoCache = {
|
|
1907
|
+
name: result?.serverInfo?.name,
|
|
1908
|
+
version: result?.serverInfo?.version,
|
|
1909
|
+
title: result?.serverInfo?.title,
|
|
1910
|
+
protocolVersion: STATELESS_PROTOCOL_VERSION,
|
|
1911
|
+
capabilities: result?.capabilities ?? {},
|
|
1912
|
+
instructions: result?.instructions
|
|
1913
|
+
};
|
|
1914
|
+
return serverInfoCache;
|
|
1915
|
+
},
|
|
1916
|
+
async listTools() {
|
|
1917
|
+
const result = await request("tools/list", {});
|
|
1918
|
+
return result?.tools ?? [];
|
|
1919
|
+
},
|
|
1920
|
+
async callTool(name, args2) {
|
|
1921
|
+
const result = await request("tools/call", {
|
|
1922
|
+
name,
|
|
1923
|
+
arguments: args2 ?? {}
|
|
1924
|
+
});
|
|
1925
|
+
const resultType = result.resultType ?? "complete";
|
|
1926
|
+
if (resultType === "input_required") {
|
|
1927
|
+
throw new InputRequiredError(
|
|
1928
|
+
JSON.stringify({ name, arguments: args2 ?? {}, result }, null, 2)
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
return result;
|
|
1932
|
+
},
|
|
1933
|
+
async close() {
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// src/client/connect.ts
|
|
1939
|
+
async function connect(platform2, options) {
|
|
1940
|
+
if (options.command) {
|
|
1941
|
+
return createPiConnection({
|
|
1942
|
+
command: options.command,
|
|
1943
|
+
args: options.args,
|
|
1944
|
+
env: options.env,
|
|
1945
|
+
cwd: options.cwd,
|
|
1946
|
+
version: options.version
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
if (!options.url) {
|
|
1950
|
+
throw new Error("connect requires either a url or a command");
|
|
1951
|
+
}
|
|
1952
|
+
if (options.protocolVersion && !options.protocolVersion.startsWith("2026")) {
|
|
1953
|
+
return createPiConnection({
|
|
1954
|
+
url: options.url,
|
|
1955
|
+
headers: options.headers,
|
|
1956
|
+
version: options.version
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
return await createStatelessConnection({
|
|
1961
|
+
url: options.url,
|
|
1962
|
+
headers: options.headers,
|
|
1963
|
+
fetchImpl: platform2.fetch,
|
|
1964
|
+
clientInfo: { name: "mcpcat", version: options.version ?? "0.0.0" }
|
|
1965
|
+
});
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
const isMethodNotFound = err instanceof StatelessRpcError && err.code === -32601 || err instanceof StatelessHttpError && err.status >= 400 && err.status < 500;
|
|
1968
|
+
if (options.protocolVersion?.startsWith("2026")) {
|
|
1969
|
+
throw err;
|
|
1970
|
+
}
|
|
1971
|
+
if (isMethodNotFound) {
|
|
1972
|
+
return createPiConnection({
|
|
1973
|
+
url: options.url,
|
|
1974
|
+
headers: options.headers,
|
|
1975
|
+
version: options.version
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
throw err;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/client/targets.ts
|
|
1983
|
+
function splitShellStyle(input) {
|
|
1984
|
+
const result = [];
|
|
1985
|
+
let current = "";
|
|
1986
|
+
let inSingle = false;
|
|
1987
|
+
let inDouble = false;
|
|
1988
|
+
for (let i = 0; i < input.length; i++) {
|
|
1989
|
+
const c = input[i];
|
|
1990
|
+
if (inSingle) {
|
|
1991
|
+
if (c === "'") {
|
|
1992
|
+
inSingle = false;
|
|
1993
|
+
} else {
|
|
1994
|
+
current += c;
|
|
1995
|
+
}
|
|
1996
|
+
} else if (inDouble) {
|
|
1997
|
+
if (c === '"') {
|
|
1998
|
+
inDouble = false;
|
|
1999
|
+
} else if (c === "\\" && i + 1 < input.length) {
|
|
2000
|
+
current += input[++i];
|
|
2001
|
+
} else {
|
|
2002
|
+
current += c;
|
|
2003
|
+
}
|
|
2004
|
+
} else if (c === "'") {
|
|
2005
|
+
inSingle = true;
|
|
2006
|
+
} else if (c === '"') {
|
|
2007
|
+
inDouble = true;
|
|
2008
|
+
} else if (c === " " || c === " ") {
|
|
2009
|
+
if (current.length > 0) {
|
|
2010
|
+
result.push(current);
|
|
2011
|
+
current = "";
|
|
2012
|
+
}
|
|
2013
|
+
} else if (c === "\\" && i + 1 < input.length) {
|
|
2014
|
+
current += input[++i];
|
|
2015
|
+
} else {
|
|
2016
|
+
current += c;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (current.length > 0) result.push(current);
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
function normalizeUrl(raw) {
|
|
2023
|
+
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
|
|
2024
|
+
if (raw.startsWith("localhost") || raw.startsWith("127.0.0.1") || raw.startsWith("[::1]")) {
|
|
2025
|
+
return `http://${raw}`;
|
|
2026
|
+
}
|
|
2027
|
+
return `https://${raw}`;
|
|
2028
|
+
}
|
|
2029
|
+
function looksLikeUrl(value) {
|
|
2030
|
+
if (value.startsWith("http://") || value.startsWith("https://")) return true;
|
|
2031
|
+
if (value.includes(".") && !value.includes(" ")) return true;
|
|
2032
|
+
if (value.startsWith("localhost") || value.startsWith("127.0.0.1") || value.startsWith("[::1]"))
|
|
2033
|
+
return true;
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
2036
|
+
async function resolveTarget(target, trailingArgs, config) {
|
|
2037
|
+
if (trailingArgs && trailingArgs.length > 0) {
|
|
2038
|
+
return { command: trailingArgs[0], args: trailingArgs.slice(1) };
|
|
2039
|
+
}
|
|
2040
|
+
if (!target) {
|
|
2041
|
+
throw new Error("no target given");
|
|
2042
|
+
}
|
|
2043
|
+
if (target.startsWith("stdio:")) {
|
|
2044
|
+
const rest = target.slice(6);
|
|
2045
|
+
const parts = splitShellStyle(rest);
|
|
2046
|
+
if (parts.length === 0) throw new Error("stdio: target requires a command");
|
|
2047
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
2048
|
+
}
|
|
2049
|
+
if (looksLikeUrl(target)) {
|
|
2050
|
+
return { url: normalizeUrl(target) };
|
|
2051
|
+
}
|
|
2052
|
+
const entry = await config.get(target);
|
|
2053
|
+
if (entry) {
|
|
2054
|
+
return configToTarget(entry);
|
|
2055
|
+
}
|
|
2056
|
+
throw new Error(`unknown target '${target}': not a URL and not found in mcp.json`);
|
|
2057
|
+
}
|
|
2058
|
+
function configToTarget(entry) {
|
|
2059
|
+
if (entry.url) {
|
|
2060
|
+
return {
|
|
2061
|
+
url: entry.url,
|
|
2062
|
+
headers: entry.headers
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
if (entry.command) {
|
|
2066
|
+
return {
|
|
2067
|
+
command: entry.command,
|
|
2068
|
+
args: entry.args,
|
|
2069
|
+
env: entry.env,
|
|
2070
|
+
cwd: entry.cwd
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
throw new Error("config entry has neither url nor command");
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/config/source.ts
|
|
2077
|
+
function createConfigSource(platform2) {
|
|
2078
|
+
const home = platform2.env.HOME ?? platform2.env.USERPROFILE ?? "";
|
|
2079
|
+
const piDir = platform2.env.PI_CODING_AGENT_DIR ?? `${home}/.pi/agent`;
|
|
2080
|
+
const configPath = `${piDir}/mcp.json`;
|
|
2081
|
+
return {
|
|
2082
|
+
async get(name) {
|
|
2083
|
+
try {
|
|
2084
|
+
const exists = await platform2.fs.exists(configPath);
|
|
2085
|
+
if (!exists) return void 0;
|
|
2086
|
+
const data = await platform2.fs.readFile(configPath);
|
|
2087
|
+
const text = new TextDecoder().decode(data);
|
|
2088
|
+
const parsed = JSON.parse(text);
|
|
2089
|
+
if (!parsed.mcpServers) return void 0;
|
|
2090
|
+
const normalizedName = name.replace(/[-_]/g, "").toLowerCase();
|
|
2091
|
+
for (const [key, value] of Object.entries(parsed.mcpServers)) {
|
|
2092
|
+
if (key.replace(/[-_]/g, "").toLowerCase() === normalizedName) {
|
|
2093
|
+
return value;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return void 0;
|
|
2097
|
+
} catch {
|
|
2098
|
+
return void 0;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
// src/cli/main.ts
|
|
2105
|
+
var VERBS = [
|
|
2106
|
+
"info",
|
|
2107
|
+
"tools",
|
|
2108
|
+
"ls",
|
|
2109
|
+
"call",
|
|
2110
|
+
"describe",
|
|
2111
|
+
"resources",
|
|
2112
|
+
"read",
|
|
2113
|
+
"prompts",
|
|
2114
|
+
"prompt",
|
|
2115
|
+
"grep",
|
|
2116
|
+
"complete",
|
|
2117
|
+
"task",
|
|
2118
|
+
"add",
|
|
2119
|
+
"rm",
|
|
2120
|
+
"list",
|
|
2121
|
+
"enable",
|
|
2122
|
+
"disable",
|
|
2123
|
+
"exposure",
|
|
2124
|
+
"import",
|
|
2125
|
+
"login",
|
|
2126
|
+
"logout",
|
|
2127
|
+
"serve",
|
|
2128
|
+
"code",
|
|
2129
|
+
"discover",
|
|
2130
|
+
"inbox",
|
|
2131
|
+
"help"
|
|
2132
|
+
];
|
|
2133
|
+
var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
2134
|
+
"resources",
|
|
2135
|
+
"read",
|
|
2136
|
+
"prompts",
|
|
2137
|
+
"prompt",
|
|
2138
|
+
"grep",
|
|
2139
|
+
"complete",
|
|
2140
|
+
"task",
|
|
2141
|
+
"add",
|
|
2142
|
+
"rm",
|
|
2143
|
+
"list",
|
|
2144
|
+
"enable",
|
|
2145
|
+
"disable",
|
|
2146
|
+
"exposure",
|
|
2147
|
+
"import",
|
|
2148
|
+
"login",
|
|
2149
|
+
"logout",
|
|
2150
|
+
"serve",
|
|
2151
|
+
"code",
|
|
2152
|
+
"discover",
|
|
2153
|
+
"inbox"
|
|
2154
|
+
]);
|
|
2155
|
+
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe"]);
|
|
2156
|
+
function helpText(version) {
|
|
2157
|
+
return `mcpcat ${version} \u2013 a curl for the Model Context Protocol
|
|
2158
|
+
|
|
2159
|
+
Usage: mcpcat [options] <target> [<tool> [args\u2026]]
|
|
2160
|
+
mcpcat [options] <verb> \u2026
|
|
2161
|
+
|
|
2162
|
+
Client verbs:
|
|
2163
|
+
info <target> Server identity and capabilities
|
|
2164
|
+
tools <target> List tools
|
|
2165
|
+
call <target> <tool> Call a tool
|
|
2166
|
+
describe <target> [tool] Tool schema and help
|
|
2167
|
+
|
|
2168
|
+
Configuration verbs:
|
|
2169
|
+
add <name> <url> Add a server to mcp.json (not implemented yet)
|
|
2170
|
+
rm <name> Remove a server (not implemented yet)
|
|
2171
|
+
list List configured servers (not implemented yet)
|
|
2172
|
+
enable|disable <name> Toggle a server (not implemented yet)
|
|
2173
|
+
exposure <name> <mode> Set exposure mode (not implemented yet)
|
|
2174
|
+
import <file> Import from other configs (not implemented yet)
|
|
2175
|
+
login <target> OAuth sign-in (not implemented yet)
|
|
2176
|
+
logout <target> Remove tokens (not implemented yet)
|
|
2177
|
+
|
|
2178
|
+
Server and code verbs:
|
|
2179
|
+
serve [--stdio|--http] \u2026 Expose commands as MCP server (not implemented yet)
|
|
2180
|
+
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
2181
|
+
discover <url> Find MCP servers (not implemented yet)
|
|
2182
|
+
inbox Discovery inbox (not implemented yet)
|
|
2183
|
+
help [verb] Show help
|
|
2184
|
+
|
|
2185
|
+
Options:
|
|
2186
|
+
--json JSON output
|
|
2187
|
+
-H, --header 'N: V' Extra header
|
|
2188
|
+
-d <json> Tool arguments as JSON (or @file, @- for stdin)
|
|
2189
|
+
-m, --timeout <s> Request timeout (default 60)
|
|
2190
|
+
--protocol-version <v> Pin protocol version
|
|
2191
|
+
--schema Print raw JSON Schema (describe)
|
|
2192
|
+
-v, --verbose Trace on stderr
|
|
2193
|
+
-s, --silent Suppress progress
|
|
2194
|
+
-o, --output <path> Write result to file
|
|
2195
|
+
-V, --version Print version
|
|
2196
|
+
-h, --help Show this help
|
|
2197
|
+
`;
|
|
2198
|
+
}
|
|
2199
|
+
function parseFlags(args2) {
|
|
2200
|
+
const headers = {};
|
|
2201
|
+
let dataArg;
|
|
2202
|
+
let protocolVersion;
|
|
2203
|
+
let showSchema = false;
|
|
2204
|
+
const positional = [];
|
|
2205
|
+
let trailingCommand;
|
|
2206
|
+
for (let i = 0; i < args2.length; i++) {
|
|
2207
|
+
const arg = args2[i];
|
|
2208
|
+
if (arg === "--") {
|
|
2209
|
+
trailingCommand = args2.slice(i + 1);
|
|
2210
|
+
break;
|
|
2211
|
+
}
|
|
2212
|
+
if ((arg === "-H" || arg === "--header") && args2[i + 1]) {
|
|
2213
|
+
const val = args2[++i];
|
|
2214
|
+
const colonIdx = val.indexOf(":");
|
|
2215
|
+
if (colonIdx > 0) {
|
|
2216
|
+
headers[val.slice(0, colonIdx).trim()] = val.slice(colonIdx + 1).trim();
|
|
2217
|
+
}
|
|
2218
|
+
} else if (arg === "-d" && args2[i + 1]) {
|
|
2219
|
+
dataArg = args2[++i];
|
|
2220
|
+
} else if (arg === "--protocol-version" && args2[i + 1]) {
|
|
2221
|
+
protocolVersion = args2[++i];
|
|
2222
|
+
} else if (arg === "--schema") {
|
|
2223
|
+
showSchema = true;
|
|
2224
|
+
} else if (!arg.startsWith("-")) {
|
|
2225
|
+
positional.push(arg);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return { headers, dataArg, protocolVersion, showSchema, positional, trailingCommand };
|
|
2229
|
+
}
|
|
2230
|
+
function resolveVerb(positional, trailingCommand) {
|
|
2231
|
+
let verb;
|
|
2232
|
+
let target;
|
|
2233
|
+
let toolName;
|
|
2234
|
+
if (positional.length >= 1 && VERBS.includes(positional[0])) {
|
|
2235
|
+
verb = positional[0];
|
|
2236
|
+
if (verb === "ls") verb = "tools";
|
|
2237
|
+
target = positional[1];
|
|
2238
|
+
toolName = positional[2];
|
|
2239
|
+
} else if (positional.length >= 2) {
|
|
2240
|
+
target = positional[0];
|
|
2241
|
+
toolName = positional[1];
|
|
2242
|
+
verb = "call";
|
|
2243
|
+
} else if (positional.length === 1) {
|
|
2244
|
+
target = positional[0];
|
|
2245
|
+
verb = "info";
|
|
2246
|
+
}
|
|
2247
|
+
if (!target && trailingCommand && trailingCommand.length > 0) {
|
|
2248
|
+
target = "__trailing__";
|
|
2249
|
+
}
|
|
2250
|
+
return {
|
|
2251
|
+
verb,
|
|
2252
|
+
target,
|
|
2253
|
+
toolName,
|
|
2254
|
+
dataArg: void 0,
|
|
2255
|
+
headers: {},
|
|
2256
|
+
protocolVersion: void 0,
|
|
2257
|
+
showSchema: false,
|
|
2258
|
+
trailingCommand
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function parseArgs(args2) {
|
|
2262
|
+
const flags = parseFlags(args2);
|
|
2263
|
+
const resolved = resolveVerb(flags.positional, flags.trailingCommand);
|
|
2264
|
+
return {
|
|
2265
|
+
...resolved,
|
|
2266
|
+
headers: flags.headers,
|
|
2267
|
+
dataArg: flags.dataArg,
|
|
2268
|
+
protocolVersion: flags.protocolVersion,
|
|
2269
|
+
showSchema: flags.showSchema
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
function formatToolLine(tool) {
|
|
2273
|
+
const desc = tool.description ? ` ${tool.description}` : "";
|
|
2274
|
+
return `${tool.name}${desc}`;
|
|
2275
|
+
}
|
|
2276
|
+
function formatToolSchema(tool) {
|
|
2277
|
+
const lines = [];
|
|
2278
|
+
lines.push(`${tool.name}`);
|
|
2279
|
+
if (tool.description) lines.push(` ${tool.description}`);
|
|
2280
|
+
if (tool.inputSchema) {
|
|
2281
|
+
lines.push(" Input schema:");
|
|
2282
|
+
lines.push(` ${JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n ")}`);
|
|
2283
|
+
}
|
|
2284
|
+
const annotations = tool.annotations;
|
|
2285
|
+
if (annotations) {
|
|
2286
|
+
lines.push(" Annotations:");
|
|
2287
|
+
for (const [k, v] of Object.entries(annotations)) {
|
|
2288
|
+
lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return lines.join("\n");
|
|
2292
|
+
}
|
|
2293
|
+
function renderCallResult(result) {
|
|
2294
|
+
const parts = [];
|
|
2295
|
+
if (result.content) {
|
|
2296
|
+
for (const block of result.content) {
|
|
2297
|
+
if (block.type === "text") parts.push(block.text);
|
|
2298
|
+
else if (block.type === "image")
|
|
2299
|
+
parts.push(`[image: ${block.mimeType}, ${Math.ceil(block.data.length * 3 / 4)} bytes]`);
|
|
2300
|
+
else if (block.type === "resource_link") parts.push(`[resource: ${block.uri}]`);
|
|
2301
|
+
else if (block.type === "resource") {
|
|
2302
|
+
parts.push(
|
|
2303
|
+
"text" in block.resource ? block.resource.text : `[blob: ${block.resource.uri}]`
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (parts.length === 0 && result.structuredContent !== void 0) {
|
|
2309
|
+
parts.push(JSON.stringify(result.structuredContent, null, 2));
|
|
2310
|
+
}
|
|
2311
|
+
return parts.join("\n");
|
|
2312
|
+
}
|
|
2313
|
+
async function readDataArg(platform2, dataArg) {
|
|
2314
|
+
let raw;
|
|
2315
|
+
if (dataArg.startsWith("@-")) {
|
|
2316
|
+
const reader = platform2.stdin.getReader();
|
|
2317
|
+
const chunks = [];
|
|
2318
|
+
for (; ; ) {
|
|
2319
|
+
const { done, value } = await reader.read();
|
|
2320
|
+
if (done) break;
|
|
2321
|
+
chunks.push(value);
|
|
2322
|
+
}
|
|
2323
|
+
reader.releaseLock();
|
|
2324
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
2325
|
+
const merged = new Uint8Array(total);
|
|
2326
|
+
let offset = 0;
|
|
2327
|
+
for (const c of chunks) {
|
|
2328
|
+
merged.set(c, offset);
|
|
2329
|
+
offset += c.length;
|
|
2330
|
+
}
|
|
2331
|
+
raw = new TextDecoder().decode(merged);
|
|
2332
|
+
} else if (dataArg.startsWith("@")) {
|
|
2333
|
+
const data = await platform2.fs.readFile(dataArg.slice(1));
|
|
2334
|
+
raw = new TextDecoder().decode(data);
|
|
2335
|
+
} else {
|
|
2336
|
+
raw = dataArg;
|
|
2337
|
+
}
|
|
2338
|
+
return JSON.parse(raw);
|
|
2339
|
+
}
|
|
2340
|
+
async function runInfo(platform2, conn) {
|
|
2341
|
+
const info = await conn.info();
|
|
2342
|
+
const lines = [];
|
|
2343
|
+
if (info.name) lines.push(`Server: ${info.name}${info.version ? ` ${info.version}` : ""}`);
|
|
2344
|
+
if (info.title) lines.push(`Title: ${info.title}`);
|
|
2345
|
+
lines.push(`Protocol: ${info.protocolVersion}`);
|
|
2346
|
+
if (info.instructions) lines.push(`Instructions: ${info.instructions}`);
|
|
2347
|
+
const caps = Object.keys(info.capabilities).filter(
|
|
2348
|
+
(k) => info.capabilities[k] != null && info.capabilities[k] !== false
|
|
2349
|
+
);
|
|
2350
|
+
if (caps.length > 0) lines.push(`Capabilities: ${caps.join(", ")}`);
|
|
2351
|
+
try {
|
|
2352
|
+
const tools = await conn.listTools();
|
|
2353
|
+
lines.push(`Tools: ${tools.length}`);
|
|
2354
|
+
} catch {
|
|
2355
|
+
}
|
|
2356
|
+
await platform2.write(1, `${lines.join("\n")}
|
|
2357
|
+
`);
|
|
2358
|
+
return 0;
|
|
2359
|
+
}
|
|
2360
|
+
async function runTools(platform2, conn) {
|
|
2361
|
+
const tools = await conn.listTools();
|
|
2362
|
+
if (tools.length === 0) {
|
|
2363
|
+
await platform2.write(1, "(no tools)\n");
|
|
2364
|
+
} else {
|
|
2365
|
+
await platform2.write(1, `${tools.map(formatToolLine).join("\n")}
|
|
2366
|
+
`);
|
|
2367
|
+
}
|
|
2368
|
+
return 0;
|
|
2369
|
+
}
|
|
2370
|
+
async function runCall(platform2, conn, toolName, dataArg) {
|
|
2371
|
+
let toolArgs = {};
|
|
2372
|
+
if (dataArg) {
|
|
2373
|
+
try {
|
|
2374
|
+
toolArgs = await readDataArg(platform2, dataArg);
|
|
2375
|
+
} catch (err) {
|
|
2376
|
+
await platform2.write(2, `mcpcat: invalid -d argument: ${err.message}
|
|
2377
|
+
`);
|
|
2378
|
+
return 2;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
const result = await conn.callTool(toolName, toolArgs);
|
|
2382
|
+
const output = renderCallResult(result);
|
|
2383
|
+
if (result.isError) {
|
|
2384
|
+
await platform2.write(2, `${output}
|
|
2385
|
+
`);
|
|
2386
|
+
return 1;
|
|
2387
|
+
}
|
|
2388
|
+
await platform2.write(1, `${output}
|
|
2389
|
+
`);
|
|
2390
|
+
return 0;
|
|
2391
|
+
}
|
|
2392
|
+
async function runDescribe(platform2, conn, toolName, showSchema) {
|
|
2393
|
+
const tools = await conn.listTools();
|
|
2394
|
+
if (toolName) {
|
|
2395
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
2396
|
+
if (!tool) {
|
|
2397
|
+
await platform2.write(2, `mcpcat: tool '${toolName}' not found
|
|
2398
|
+
`);
|
|
2399
|
+
return 2;
|
|
2400
|
+
}
|
|
2401
|
+
if (showSchema && tool.inputSchema) {
|
|
2402
|
+
await platform2.write(1, `${JSON.stringify(tool.inputSchema, null, 2)}
|
|
2403
|
+
`);
|
|
2404
|
+
} else {
|
|
2405
|
+
await platform2.write(1, `${formatToolSchema(tool)}
|
|
2406
|
+
`);
|
|
2407
|
+
}
|
|
2408
|
+
} else {
|
|
2409
|
+
for (const tool of tools) {
|
|
2410
|
+
await platform2.write(1, `${formatToolSchema(tool)}
|
|
2411
|
+
|
|
2412
|
+
`);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return 0;
|
|
2416
|
+
}
|
|
2417
|
+
async function runVerb(platform2, parsed, conn) {
|
|
2418
|
+
switch (parsed.verb) {
|
|
2419
|
+
case "info":
|
|
2420
|
+
return runInfo(platform2, conn);
|
|
2421
|
+
case "tools":
|
|
2422
|
+
return runTools(platform2, conn);
|
|
2423
|
+
case "call":
|
|
2424
|
+
return runCall(platform2, conn, parsed.toolName, parsed.dataArg);
|
|
2425
|
+
case "describe":
|
|
2426
|
+
return runDescribe(platform2, conn, parsed.toolName, parsed.showSchema);
|
|
2427
|
+
default:
|
|
2428
|
+
await platform2.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
2429
|
+
`);
|
|
2430
|
+
return 2;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
async function run(platform2, version, args2) {
|
|
2434
|
+
if (args2.includes("--version") || args2.includes("-V")) {
|
|
2435
|
+
await platform2.write(1, `${version}
|
|
2436
|
+
`);
|
|
2437
|
+
return 0;
|
|
2438
|
+
}
|
|
2439
|
+
if (args2.includes("--help") || args2.includes("-h") || args2[0] === "help") {
|
|
2440
|
+
await platform2.write(1, helpText(version));
|
|
2441
|
+
return 0;
|
|
2442
|
+
}
|
|
2443
|
+
if (args2.length === 0) {
|
|
2444
|
+
await platform2.write(1, helpText(version));
|
|
2445
|
+
return 0;
|
|
2446
|
+
}
|
|
2447
|
+
const parsed = parseArgs(args2);
|
|
2448
|
+
if (!parsed.verb) {
|
|
2449
|
+
await platform2.write(2, "mcpcat: no verb or target given\n");
|
|
2450
|
+
return 2;
|
|
2451
|
+
}
|
|
2452
|
+
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
2453
|
+
await platform2.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
2454
|
+
`);
|
|
2455
|
+
return 1;
|
|
2456
|
+
}
|
|
2457
|
+
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
2458
|
+
await platform2.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
2459
|
+
`);
|
|
2460
|
+
return 2;
|
|
2461
|
+
}
|
|
2462
|
+
if (parsed.verb === "call" && !parsed.toolName) {
|
|
2463
|
+
await platform2.write(2, "mcpcat: call requires a tool name\n");
|
|
2464
|
+
return 2;
|
|
2465
|
+
}
|
|
2466
|
+
if (!parsed.target) {
|
|
2467
|
+
await platform2.write(2, `mcpcat: ${parsed.verb} requires a target
|
|
2468
|
+
`);
|
|
2469
|
+
return 2;
|
|
2470
|
+
}
|
|
2471
|
+
const config = createConfigSource(platform2);
|
|
2472
|
+
let resolved;
|
|
2473
|
+
try {
|
|
2474
|
+
resolved = await resolveTarget(
|
|
2475
|
+
parsed.target === "__trailing__" ? "" : parsed.target,
|
|
2476
|
+
parsed.trailingCommand,
|
|
2477
|
+
config
|
|
2478
|
+
);
|
|
2479
|
+
} catch (err) {
|
|
2480
|
+
await platform2.write(2, `mcpcat: ${err.message}
|
|
2481
|
+
`);
|
|
2482
|
+
return 2;
|
|
2483
|
+
}
|
|
2484
|
+
let conn;
|
|
2485
|
+
try {
|
|
2486
|
+
conn = await connect(platform2, {
|
|
2487
|
+
...resolved,
|
|
2488
|
+
headers: { ...resolved.headers, ...parsed.headers },
|
|
2489
|
+
protocolVersion: parsed.protocolVersion,
|
|
2490
|
+
version
|
|
2491
|
+
});
|
|
2492
|
+
} catch (err) {
|
|
2493
|
+
await platform2.write(2, `mcpcat: connection failed: ${err.message}
|
|
2494
|
+
`);
|
|
2495
|
+
return 3;
|
|
2496
|
+
}
|
|
2497
|
+
try {
|
|
2498
|
+
return await runVerb(platform2, parsed, conn);
|
|
2499
|
+
} catch (err) {
|
|
2500
|
+
if (err instanceof InputRequiredError) {
|
|
2501
|
+
await platform2.write(2, `${err.requestJson}
|
|
2502
|
+
`);
|
|
2503
|
+
return 6;
|
|
2504
|
+
}
|
|
2505
|
+
await platform2.write(2, `mcpcat: ${err.message}
|
|
2506
|
+
`);
|
|
2507
|
+
return 5;
|
|
2508
|
+
} finally {
|
|
2509
|
+
try {
|
|
2510
|
+
await conn.close();
|
|
2511
|
+
} catch {
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/platform/host.ts
|
|
2517
|
+
import { spawn as cpSpawn } from "node:child_process";
|
|
2518
|
+
import { constants } from "node:fs";
|
|
2519
|
+
import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
2520
|
+
import { Readable, Writable } from "node:stream";
|
|
2521
|
+
import * as tty from "node:tty";
|
|
2522
|
+
function nodeSpawn(options) {
|
|
2523
|
+
const child = cpSpawn(options.argv[0], options.argv.slice(1), {
|
|
2524
|
+
env: options.env ?? process.env,
|
|
2525
|
+
cwd: options.cwd,
|
|
2526
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2527
|
+
});
|
|
2528
|
+
const stdout = Readable.toWeb(child.stdout);
|
|
2529
|
+
const stderr = Readable.toWeb(child.stderr);
|
|
2530
|
+
const stdin = Writable.toWeb(child.stdin);
|
|
2531
|
+
return Promise.resolve({
|
|
2532
|
+
stdin,
|
|
2533
|
+
stdout,
|
|
2534
|
+
stderr,
|
|
2535
|
+
wait() {
|
|
2536
|
+
return new Promise((resolve) => {
|
|
2537
|
+
child.on("close", (code2, signal) => {
|
|
2538
|
+
resolve({
|
|
2539
|
+
status: signal ? 128 + (signalNumber(signal) ?? 0) : code2 ?? 1,
|
|
2540
|
+
signal: signal ?? void 0
|
|
2541
|
+
});
|
|
2542
|
+
});
|
|
2543
|
+
});
|
|
2544
|
+
},
|
|
2545
|
+
kill(signal) {
|
|
2546
|
+
child.kill(signal ?? "SIGTERM");
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
function signalNumber(sig) {
|
|
2551
|
+
const map = {
|
|
2552
|
+
SIGHUP: 1,
|
|
2553
|
+
SIGINT: 2,
|
|
2554
|
+
SIGQUIT: 3,
|
|
2555
|
+
SIGTERM: 15,
|
|
2556
|
+
SIGKILL: 9
|
|
2557
|
+
};
|
|
2558
|
+
return map[sig];
|
|
2559
|
+
}
|
|
2560
|
+
var hostFs = {
|
|
2561
|
+
async readFile(path) {
|
|
2562
|
+
return new Uint8Array(await readFile(path));
|
|
2563
|
+
},
|
|
2564
|
+
async writeFile(path, data) {
|
|
2565
|
+
await writeFile(path, data);
|
|
2566
|
+
},
|
|
2567
|
+
async mkdir(path) {
|
|
2568
|
+
await mkdir(path, { recursive: true });
|
|
2569
|
+
},
|
|
2570
|
+
async rename(from, to) {
|
|
2571
|
+
await rename(from, to);
|
|
2572
|
+
},
|
|
2573
|
+
async stat(path) {
|
|
2574
|
+
const s = await stat(path);
|
|
2575
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
2576
|
+
},
|
|
2577
|
+
async exists(path) {
|
|
2578
|
+
try {
|
|
2579
|
+
await access(path, constants.F_OK);
|
|
2580
|
+
return true;
|
|
2581
|
+
} catch {
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
};
|
|
2586
|
+
function createStdinStream() {
|
|
2587
|
+
return Readable.toWeb(process.stdin);
|
|
2588
|
+
}
|
|
2589
|
+
function createStdoutStream() {
|
|
2590
|
+
return Writable.toWeb(process.stdout);
|
|
2591
|
+
}
|
|
2592
|
+
function createStderrStream() {
|
|
2593
|
+
return Writable.toWeb(process.stderr);
|
|
2594
|
+
}
|
|
2595
|
+
function createHostPlatform() {
|
|
2596
|
+
const encoder = new TextEncoder();
|
|
2597
|
+
return {
|
|
2598
|
+
fetch: globalThis.fetch,
|
|
2599
|
+
spawn: nodeSpawn,
|
|
2600
|
+
fs: hostFs,
|
|
2601
|
+
env: process.env,
|
|
2602
|
+
cwd: process.cwd(),
|
|
2603
|
+
isatty(fd) {
|
|
2604
|
+
return tty.isatty(fd);
|
|
2605
|
+
},
|
|
2606
|
+
stdin: createStdinStream(),
|
|
2607
|
+
stdout: createStdoutStream(),
|
|
2608
|
+
stderr: createStderrStream(),
|
|
2609
|
+
async write(fd, data) {
|
|
2610
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
2611
|
+
const stream = fd === 1 ? process.stdout : process.stderr;
|
|
2612
|
+
return new Promise((resolve, reject) => {
|
|
2613
|
+
stream.write(bytes, (err) => err ? reject(err) : resolve());
|
|
2614
|
+
});
|
|
2615
|
+
},
|
|
2616
|
+
exit(code2) {
|
|
2617
|
+
process.exit(code2);
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// src/host.ts
|
|
2623
|
+
var platform = createHostPlatform();
|
|
2624
|
+
var args = process.argv.slice(2);
|
|
2625
|
+
var code = await run(platform, "0.2.0", args);
|
|
2626
|
+
process.exit(code);
|