@ai-ecoverse/mcpcat 0.3.0 → 0.5.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/dist/host/mcpcat.mjs +1730 -33
- package/dist/kernel/mcpcat.mjs +1697 -34
- package/package.json +8 -4
package/dist/kernel/mcpcat.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
var __mcpcat_byteLength = (s) => new TextEncoder().encode(s).length;
|
|
2
|
+
|
|
1
3
|
// node_modules/@earendil-works/pi-mcp/dist/protocol/jsonrpc.js
|
|
2
4
|
var JSON_RPC_ERROR_CODES = {
|
|
3
5
|
parseError: -32700,
|
|
@@ -738,7 +740,7 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 3e4;
|
|
|
738
740
|
var DEFAULT_RECONNECT_MAX_RETRIES = 5;
|
|
739
741
|
async function consumeSseStream(stream, options) {
|
|
740
742
|
const reader = stream.getReader();
|
|
741
|
-
const
|
|
743
|
+
const decoder2 = new TextDecoder();
|
|
742
744
|
let buffered = "";
|
|
743
745
|
let eventName;
|
|
744
746
|
let eventId;
|
|
@@ -772,7 +774,7 @@ async function consumeSseStream(stream, options) {
|
|
|
772
774
|
if (value.startsWith(" "))
|
|
773
775
|
value = value.slice(1);
|
|
774
776
|
if (field === "data") {
|
|
775
|
-
dataBytes +=
|
|
777
|
+
dataBytes += __mcpcat_byteLength(value) + (dataLines.length > 0 ? 1 : 0);
|
|
776
778
|
if (dataBytes > maxEventBytes)
|
|
777
779
|
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
778
780
|
dataLines.push(value);
|
|
@@ -789,17 +791,17 @@ async function consumeSseStream(stream, options) {
|
|
|
789
791
|
const { done, value } = await reader.read();
|
|
790
792
|
if (done)
|
|
791
793
|
break;
|
|
792
|
-
buffered +=
|
|
794
|
+
buffered += decoder2.decode(value, { stream: true });
|
|
793
795
|
let newline = buffered.indexOf("\n");
|
|
794
796
|
while (newline >= 0) {
|
|
795
797
|
processLine(buffered.slice(0, newline));
|
|
796
798
|
buffered = buffered.slice(newline + 1);
|
|
797
799
|
newline = buffered.indexOf("\n");
|
|
798
800
|
}
|
|
799
|
-
if (
|
|
801
|
+
if (__mcpcat_byteLength(buffered) > maxEventBytes)
|
|
800
802
|
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
801
803
|
}
|
|
802
|
-
buffered +=
|
|
804
|
+
buffered += decoder2.decode();
|
|
803
805
|
if (buffered)
|
|
804
806
|
processLine(buffered);
|
|
805
807
|
dispatch();
|
|
@@ -1173,6 +1175,185 @@ var StreamableHttpTransport = class extends TransportEvents {
|
|
|
1173
1175
|
}
|
|
1174
1176
|
};
|
|
1175
1177
|
|
|
1178
|
+
// src/client/platform-stdio-transport.ts
|
|
1179
|
+
var DEFAULT_MAX_MESSAGE_BYTES2 = 4 * 1024 * 1024;
|
|
1180
|
+
var DEFAULT_MAX_STDERR_BYTES2 = 64 * 1024;
|
|
1181
|
+
var DEFAULT_CLOSE_TIMEOUT_MS2 = 2e3;
|
|
1182
|
+
var STDIN_CLOSE_GRACE_MS2 = 500;
|
|
1183
|
+
var PlatformStdioTransport = class {
|
|
1184
|
+
options;
|
|
1185
|
+
process;
|
|
1186
|
+
started = false;
|
|
1187
|
+
closed = false;
|
|
1188
|
+
closeEmitted = false;
|
|
1189
|
+
stderrText = "";
|
|
1190
|
+
decoder = new TextDecoder();
|
|
1191
|
+
encoder = new TextEncoder();
|
|
1192
|
+
messageListeners = [];
|
|
1193
|
+
errorListeners = [];
|
|
1194
|
+
closeListeners = [];
|
|
1195
|
+
constructor(options) {
|
|
1196
|
+
this.options = options;
|
|
1197
|
+
}
|
|
1198
|
+
get stderr() {
|
|
1199
|
+
return this.stderrText;
|
|
1200
|
+
}
|
|
1201
|
+
onMessage(listener) {
|
|
1202
|
+
this.messageListeners.push(listener);
|
|
1203
|
+
return () => {
|
|
1204
|
+
this.messageListeners = this.messageListeners.filter((l) => l !== listener);
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
onError(listener) {
|
|
1208
|
+
this.errorListeners.push(listener);
|
|
1209
|
+
return () => {
|
|
1210
|
+
this.errorListeners = this.errorListeners.filter((l) => l !== listener);
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
onClose(listener) {
|
|
1214
|
+
this.closeListeners.push(listener);
|
|
1215
|
+
return () => {
|
|
1216
|
+
this.closeListeners = this.closeListeners.filter((l) => l !== listener);
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
emitMessage(message) {
|
|
1220
|
+
for (const l of [...this.messageListeners]) l(message);
|
|
1221
|
+
}
|
|
1222
|
+
emitError(error) {
|
|
1223
|
+
for (const l of [...this.errorListeners]) l(error);
|
|
1224
|
+
}
|
|
1225
|
+
emitClose() {
|
|
1226
|
+
if (this.closeEmitted) return;
|
|
1227
|
+
this.closeEmitted = true;
|
|
1228
|
+
for (const l of [...this.closeListeners]) l();
|
|
1229
|
+
}
|
|
1230
|
+
async start() {
|
|
1231
|
+
if (this.started) throw new Error("MCP stdio transport already started");
|
|
1232
|
+
if (this.closed) throw new Error("MCP connection closed");
|
|
1233
|
+
this.started = true;
|
|
1234
|
+
const env = this.options.inheritEnv === false ? { ...this.options.env ?? {} } : {
|
|
1235
|
+
...this.options.platform.env,
|
|
1236
|
+
...this.options.env
|
|
1237
|
+
};
|
|
1238
|
+
this.process = await this.options.platform.spawn({
|
|
1239
|
+
argv: [this.options.command, ...this.options.args ?? []],
|
|
1240
|
+
env,
|
|
1241
|
+
cwd: this.options.cwd
|
|
1242
|
+
});
|
|
1243
|
+
void this.readStdout();
|
|
1244
|
+
void this.readStderr();
|
|
1245
|
+
void this.waitForExit();
|
|
1246
|
+
}
|
|
1247
|
+
async send(message) {
|
|
1248
|
+
if (!this.started || this.closed || !this.process) throw new Error("MCP connection closed");
|
|
1249
|
+
const payload = `${JSON.stringify(message)}
|
|
1250
|
+
`;
|
|
1251
|
+
const writer = this.process.stdin.getWriter();
|
|
1252
|
+
try {
|
|
1253
|
+
await writer.write(this.encoder.encode(payload));
|
|
1254
|
+
} finally {
|
|
1255
|
+
writer.releaseLock();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async close() {
|
|
1259
|
+
if (this.closed) return;
|
|
1260
|
+
this.closed = true;
|
|
1261
|
+
const proc = this.process;
|
|
1262
|
+
if (!proc) {
|
|
1263
|
+
this.emitClose();
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const closeTimeoutMs = this.options.closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS2;
|
|
1267
|
+
const grace = Math.min(STDIN_CLOSE_GRACE_MS2, closeTimeoutMs);
|
|
1268
|
+
const done = proc.wait().then(() => true);
|
|
1269
|
+
const delay = (ms) => new Promise((r) => setTimeout(() => r(false), ms));
|
|
1270
|
+
try {
|
|
1271
|
+
await proc.stdin.close();
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
const exited = await Promise.race([done, delay(grace)]);
|
|
1275
|
+
if (!exited) {
|
|
1276
|
+
proc.kill("SIGTERM");
|
|
1277
|
+
const exitedAfterTerm = await Promise.race([done, delay(closeTimeoutMs)]);
|
|
1278
|
+
if (!exitedAfterTerm) {
|
|
1279
|
+
proc.kill("SIGKILL");
|
|
1280
|
+
await done;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
proc.kill("SIGTERM");
|
|
1284
|
+
this.emitClose();
|
|
1285
|
+
}
|
|
1286
|
+
async readStdout() {
|
|
1287
|
+
const proc = this.process;
|
|
1288
|
+
if (!proc) return;
|
|
1289
|
+
const reader = proc.stdout.getReader();
|
|
1290
|
+
const maxBytes = this.options.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES2;
|
|
1291
|
+
let buffer = "";
|
|
1292
|
+
try {
|
|
1293
|
+
for (; ; ) {
|
|
1294
|
+
const { done, value } = await reader.read();
|
|
1295
|
+
if (done) break;
|
|
1296
|
+
buffer += this.decoder.decode(value, { stream: true });
|
|
1297
|
+
let nlIdx = buffer.indexOf("\n");
|
|
1298
|
+
while (nlIdx >= 0) {
|
|
1299
|
+
const line = buffer.slice(0, nlIdx).replace(/\r$/, "");
|
|
1300
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
1301
|
+
if (line.trim()) {
|
|
1302
|
+
try {
|
|
1303
|
+
this.emitMessage(parseJsonRpcMessage(JSON.parse(line)));
|
|
1304
|
+
} catch (err) {
|
|
1305
|
+
this.emitError(err);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
nlIdx = buffer.indexOf("\n");
|
|
1309
|
+
}
|
|
1310
|
+
if (this.encoder.encode(buffer).length > maxBytes) {
|
|
1311
|
+
buffer = "";
|
|
1312
|
+
this.emitError(new Error(`MCP stdio message exceeds ${maxBytes} bytes`));
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
buffer += this.decoder.decode();
|
|
1316
|
+
if (buffer.trim()) {
|
|
1317
|
+
this.emitError(new Error("MCP stdio server closed with an incomplete JSON-RPC message"));
|
|
1318
|
+
}
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
if (!this.closed) this.emitError(err);
|
|
1321
|
+
} finally {
|
|
1322
|
+
reader.releaseLock();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async readStderr() {
|
|
1326
|
+
const proc = this.process;
|
|
1327
|
+
if (!proc) return;
|
|
1328
|
+
const reader = proc.stderr.getReader();
|
|
1329
|
+
const maxBytes = this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES2;
|
|
1330
|
+
try {
|
|
1331
|
+
for (; ; ) {
|
|
1332
|
+
const { done, value } = await reader.read();
|
|
1333
|
+
if (done) break;
|
|
1334
|
+
const chunk = this.decoder.decode(value, { stream: true });
|
|
1335
|
+
this.stderrText += chunk;
|
|
1336
|
+
if (this.encoder.encode(this.stderrText).length > maxBytes) {
|
|
1337
|
+
this.stderrText = this.stderrText.slice(-maxBytes);
|
|
1338
|
+
}
|
|
1339
|
+
this.options.onStderr?.(chunk);
|
|
1340
|
+
}
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
async waitForExit() {
|
|
1345
|
+
const proc = this.process;
|
|
1346
|
+
if (!proc) return;
|
|
1347
|
+
try {
|
|
1348
|
+
await proc.wait();
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
if (!this.closed) {
|
|
1352
|
+
this.emitClose();
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1176
1357
|
// src/client/pi-connection.ts
|
|
1177
1358
|
async function createPiConnection(options) {
|
|
1178
1359
|
const clientOptions = {
|
|
@@ -1184,17 +1365,29 @@ async function createPiConnection(options) {
|
|
|
1184
1365
|
if (options.url) {
|
|
1185
1366
|
transport = new StreamableHttpTransport({
|
|
1186
1367
|
url: options.url,
|
|
1187
|
-
headers: options.headers
|
|
1368
|
+
headers: options.headers,
|
|
1369
|
+
fetch: options.fetchImpl
|
|
1188
1370
|
});
|
|
1189
1371
|
} else if (options.command) {
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1372
|
+
if (options.platform) {
|
|
1373
|
+
transport = new PlatformStdioTransport({
|
|
1374
|
+
command: options.command,
|
|
1375
|
+
args: options.args,
|
|
1376
|
+
env: options.env,
|
|
1377
|
+
cwd: options.cwd,
|
|
1378
|
+
inheritEnv: true,
|
|
1379
|
+
platform: options.platform
|
|
1380
|
+
});
|
|
1381
|
+
} else {
|
|
1382
|
+
transport = new StdioTransport({
|
|
1383
|
+
command: options.command,
|
|
1384
|
+
args: options.args,
|
|
1385
|
+
env: options.env,
|
|
1386
|
+
cwd: options.cwd,
|
|
1387
|
+
inheritEnv: true,
|
|
1388
|
+
stderr: "pipe"
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1198
1391
|
} else {
|
|
1199
1392
|
throw new Error("PiConnection requires either a url or a command");
|
|
1200
1393
|
}
|
|
@@ -1327,16 +1520,16 @@ async function createStatelessConnection(options) {
|
|
|
1327
1520
|
}
|
|
1328
1521
|
if (res.status >= 400) {
|
|
1329
1522
|
const text = await res.text();
|
|
1330
|
-
let
|
|
1523
|
+
let rpcError2;
|
|
1331
1524
|
try {
|
|
1332
1525
|
const parsed = JSON.parse(text);
|
|
1333
1526
|
if (parsed.error && typeof parsed.error.code === "number") {
|
|
1334
|
-
|
|
1527
|
+
rpcError2 = parsed.error;
|
|
1335
1528
|
}
|
|
1336
1529
|
} catch {
|
|
1337
1530
|
}
|
|
1338
|
-
if (
|
|
1339
|
-
throw new StatelessRpcError(
|
|
1531
|
+
if (rpcError2) {
|
|
1532
|
+
throw new StatelessRpcError(rpcError2.code, rpcError2.message, rpcError2.data);
|
|
1340
1533
|
}
|
|
1341
1534
|
throw new StatelessHttpError(res.status, res.statusText, method, text.slice(0, 512));
|
|
1342
1535
|
}
|
|
@@ -1353,10 +1546,10 @@ async function createStatelessConnection(options) {
|
|
|
1353
1546
|
}
|
|
1354
1547
|
return frame.result;
|
|
1355
1548
|
}
|
|
1356
|
-
const
|
|
1549
|
+
const discoverResult2 = await request("server/discover", {
|
|
1357
1550
|
protocolVersion: STATELESS_PROTOCOL_VERSION
|
|
1358
1551
|
});
|
|
1359
|
-
const supported = Array.isArray(
|
|
1552
|
+
const supported = Array.isArray(discoverResult2?.supportedVersions) ? discoverResult2.supportedVersions.filter((v) => typeof v === "string") : [];
|
|
1360
1553
|
if (!supported.includes(STATELESS_PROTOCOL_VERSION)) {
|
|
1361
1554
|
throw new Error(
|
|
1362
1555
|
`Server does not support ${STATELESS_PROTOCOL_VERSION} (advertised: ${supported.join(", ") || "none"})`
|
|
@@ -1367,7 +1560,7 @@ async function createStatelessConnection(options) {
|
|
|
1367
1560
|
return {
|
|
1368
1561
|
async info() {
|
|
1369
1562
|
if (serverInfoCache) return serverInfoCache;
|
|
1370
|
-
const result =
|
|
1563
|
+
const result = discoverResult2;
|
|
1371
1564
|
serverInfoCache = {
|
|
1372
1565
|
name: result?.serverInfo?.name,
|
|
1373
1566
|
version: result?.serverInfo?.version,
|
|
@@ -1408,7 +1601,8 @@ async function connect(platform, options) {
|
|
|
1408
1601
|
args: options.args,
|
|
1409
1602
|
env: options.env,
|
|
1410
1603
|
cwd: options.cwd,
|
|
1411
|
-
version: options.version
|
|
1604
|
+
version: options.version,
|
|
1605
|
+
platform
|
|
1412
1606
|
});
|
|
1413
1607
|
}
|
|
1414
1608
|
if (!options.url) {
|
|
@@ -1418,7 +1612,9 @@ async function connect(platform, options) {
|
|
|
1418
1612
|
return createPiConnection({
|
|
1419
1613
|
url: options.url,
|
|
1420
1614
|
headers: options.headers,
|
|
1421
|
-
version: options.version
|
|
1615
|
+
version: options.version,
|
|
1616
|
+
fetchImpl: platform.fetch,
|
|
1617
|
+
platform
|
|
1422
1618
|
});
|
|
1423
1619
|
}
|
|
1424
1620
|
try {
|
|
@@ -1437,7 +1633,9 @@ async function connect(platform, options) {
|
|
|
1437
1633
|
return createPiConnection({
|
|
1438
1634
|
url: options.url,
|
|
1439
1635
|
headers: options.headers,
|
|
1440
|
-
version: options.version
|
|
1636
|
+
version: options.version,
|
|
1637
|
+
fetchImpl: platform.fetch,
|
|
1638
|
+
platform
|
|
1441
1639
|
});
|
|
1442
1640
|
}
|
|
1443
1641
|
throw err;
|
|
@@ -1858,6 +2056,1399 @@ var MergedConfigSource = class _MergedConfigSource {
|
|
|
1858
2056
|
}
|
|
1859
2057
|
};
|
|
1860
2058
|
|
|
2059
|
+
// src/config/expand.ts
|
|
2060
|
+
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2061
|
+
var PLACEHOLDER_TEST = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
2062
|
+
function containsPlaceholders(value) {
|
|
2063
|
+
return PLACEHOLDER_TEST.test(value);
|
|
2064
|
+
}
|
|
2065
|
+
function expandPlaceholdersStrict(value, env) {
|
|
2066
|
+
let hasUndefined = false;
|
|
2067
|
+
const result = value.replace(PLACEHOLDER, (_, name) => {
|
|
2068
|
+
if (env[name] === void 0) {
|
|
2069
|
+
hasUndefined = true;
|
|
2070
|
+
return "";
|
|
2071
|
+
}
|
|
2072
|
+
return env[name];
|
|
2073
|
+
});
|
|
2074
|
+
if (hasUndefined) return void 0;
|
|
2075
|
+
return result;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// src/serve/catalog.ts
|
|
2079
|
+
var NAME_RE = /^[A-Za-z][\w-]*$/;
|
|
2080
|
+
function sanitizeName(raw) {
|
|
2081
|
+
const cleaned = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2082
|
+
return cleaned || "cli";
|
|
2083
|
+
}
|
|
2084
|
+
function cliPrefix(help, filePath, explicit) {
|
|
2085
|
+
if (explicit) return sanitizeName(explicit);
|
|
2086
|
+
const fromUsage = usageProgramName(help);
|
|
2087
|
+
if (fromUsage) return sanitizeName(fromUsage);
|
|
2088
|
+
const base = filePath.split("/").pop() ?? filePath;
|
|
2089
|
+
return sanitizeName(base.replace(/\.jsh$/i, ""));
|
|
2090
|
+
}
|
|
2091
|
+
function parseHelp(help) {
|
|
2092
|
+
const block = commandsFromBlock(help);
|
|
2093
|
+
const program = usageProgramName(help);
|
|
2094
|
+
const commands = mergeCommandLists(block.commands, usageCommands(help, program));
|
|
2095
|
+
return {
|
|
2096
|
+
...program ? { program } : {},
|
|
2097
|
+
commands: applyScopedFlags(commands, block.scoped),
|
|
2098
|
+
globalFlags: block.globalFlags
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
function isGroupCandidate(command) {
|
|
2102
|
+
return command.path.length === 1 && command.positionals.length === 0 && command.flags.length === 0;
|
|
2103
|
+
}
|
|
2104
|
+
function nestGroup(commands, group, nested, topNames) {
|
|
2105
|
+
const children = attachGlobalFlags(nested.commands, nested.globalFlags);
|
|
2106
|
+
if (children.length === 0) return null;
|
|
2107
|
+
const childNames = children.map((child) => child.path[0] ?? "");
|
|
2108
|
+
const same = childNames.length === topNames.size && childNames.every((name) => topNames.has(name));
|
|
2109
|
+
if (same) return null;
|
|
2110
|
+
const nestedCommands = children.map((child) => ({ ...child, path: childPath(group, child) }));
|
|
2111
|
+
return commands.filter((command) => command.path[0] !== group).concat(nestedCommands);
|
|
2112
|
+
}
|
|
2113
|
+
function attachGlobalFlags(commands, globalFlags) {
|
|
2114
|
+
if (globalFlags.length === 0) return commands;
|
|
2115
|
+
return commands.map((command) => ({ ...command, flags: mergeFlags(command.flags, globalFlags) }));
|
|
2116
|
+
}
|
|
2117
|
+
function parseMcpOverride(stdout) {
|
|
2118
|
+
let value;
|
|
2119
|
+
try {
|
|
2120
|
+
value = JSON.parse(stdout);
|
|
2121
|
+
} catch {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2125
|
+
const commandsValue = Object.getOwnPropertyDescriptor(value, "commands")?.value;
|
|
2126
|
+
if (!Array.isArray(commandsValue)) return null;
|
|
2127
|
+
const commands = [];
|
|
2128
|
+
for (const entry of commandsValue) {
|
|
2129
|
+
const command = readMcpCommand(entry);
|
|
2130
|
+
if (command) commands.push(command);
|
|
2131
|
+
}
|
|
2132
|
+
if (commands.length === 0) return null;
|
|
2133
|
+
return { commands, globalFlags: [] };
|
|
2134
|
+
}
|
|
2135
|
+
function buildTools(prefix, commands) {
|
|
2136
|
+
const tools = [
|
|
2137
|
+
{
|
|
2138
|
+
name: `${prefix}_help`,
|
|
2139
|
+
description: `Show ${prefix} help.`,
|
|
2140
|
+
inputSchema: objectSchema({}),
|
|
2141
|
+
cli: prefix,
|
|
2142
|
+
kind: "help",
|
|
2143
|
+
commandPath: []
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
name: `${prefix}_invoke`,
|
|
2147
|
+
description: `Run ${prefix} with an argv array (one element per argument) and optional stdin. A non-zero exit is a tool error.`,
|
|
2148
|
+
inputSchema: invokeSchema(),
|
|
2149
|
+
cli: prefix,
|
|
2150
|
+
kind: "invoke",
|
|
2151
|
+
commandPath: []
|
|
2152
|
+
}
|
|
2153
|
+
];
|
|
2154
|
+
for (const command of commands) {
|
|
2155
|
+
const leaf = command.path.join("_");
|
|
2156
|
+
tools.push({
|
|
2157
|
+
name: `${prefix}_${leaf}`,
|
|
2158
|
+
description: command.description || `Run ${prefix} ${command.path.join(" ")}`,
|
|
2159
|
+
inputSchema: commandSchema(command),
|
|
2160
|
+
cli: prefix,
|
|
2161
|
+
kind: "command",
|
|
2162
|
+
commandPath: command.path
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
return tools;
|
|
2166
|
+
}
|
|
2167
|
+
function readToolArguments(input) {
|
|
2168
|
+
if (input === void 0 || input === null) return { fields: [] };
|
|
2169
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
2170
|
+
return { fields: [], error: "arguments must be an object" };
|
|
2171
|
+
}
|
|
2172
|
+
const fields = [];
|
|
2173
|
+
let stdin;
|
|
2174
|
+
let passthrough;
|
|
2175
|
+
let argv;
|
|
2176
|
+
for (const key of Object.keys(input)) {
|
|
2177
|
+
const value = Object.getOwnPropertyDescriptor(input, key)?.value;
|
|
2178
|
+
const read = readArgument(key, value);
|
|
2179
|
+
if (read.error) return { fields, error: read.error };
|
|
2180
|
+
if (read.stdin !== void 0) stdin = read.stdin;
|
|
2181
|
+
else if (read.passthrough) passthrough = read.passthrough;
|
|
2182
|
+
else if (read.argv) argv = read.argv;
|
|
2183
|
+
else if (read.field) fields.push(read.field);
|
|
2184
|
+
}
|
|
2185
|
+
return {
|
|
2186
|
+
fields,
|
|
2187
|
+
...stdin !== void 0 ? { stdin } : {},
|
|
2188
|
+
...passthrough ? { passthrough } : {},
|
|
2189
|
+
...argv ? { argv } : {}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
function argvForCommand(command, args) {
|
|
2193
|
+
if (args.error) return { error: args.error };
|
|
2194
|
+
const argv = [...command.path];
|
|
2195
|
+
for (const positional of command.positionals) {
|
|
2196
|
+
const found = args.fields.find((field) => field.name === positional.name);
|
|
2197
|
+
if (!found) {
|
|
2198
|
+
if (positional.required) return { error: `missing ${positional.name}` };
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
if (typeof found.value !== "string" && typeof found.value !== "number") {
|
|
2202
|
+
return { error: `${positional.name} must be a string` };
|
|
2203
|
+
}
|
|
2204
|
+
argv.push(String(found.value));
|
|
2205
|
+
}
|
|
2206
|
+
const flags = appendFlags(command.flags, args.fields);
|
|
2207
|
+
if ("error" in flags) return flags;
|
|
2208
|
+
argv.push(...flags.argv);
|
|
2209
|
+
if (args.passthrough) argv.push(...args.passthrough);
|
|
2210
|
+
return { argv, ...args.stdin !== void 0 ? { stdin: args.stdin } : {} };
|
|
2211
|
+
}
|
|
2212
|
+
function appendFlags(flags, fields) {
|
|
2213
|
+
const argv = [];
|
|
2214
|
+
for (const flag of flags) {
|
|
2215
|
+
const found = fields.find((field) => field.name === flag.name);
|
|
2216
|
+
if (!found || found.value === false) continue;
|
|
2217
|
+
const encoded = encodeFlag(flag, found.value);
|
|
2218
|
+
if ("error" in encoded) return encoded;
|
|
2219
|
+
argv.push(...encoded.argv);
|
|
2220
|
+
}
|
|
2221
|
+
return { argv };
|
|
2222
|
+
}
|
|
2223
|
+
function encodeFlag(flag, value) {
|
|
2224
|
+
if (flag.type === "boolean") {
|
|
2225
|
+
return value === true ? { argv: [`--${flag.name}`] } : { argv: [] };
|
|
2226
|
+
}
|
|
2227
|
+
if (flag.type === "string[]") {
|
|
2228
|
+
if (!Array.isArray(value)) return { error: `${flag.name} must be an array of strings` };
|
|
2229
|
+
const argv = [];
|
|
2230
|
+
for (const item of value) argv.push(`--${flag.name}`, item);
|
|
2231
|
+
return { argv };
|
|
2232
|
+
}
|
|
2233
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
2234
|
+
return { error: `${flag.name} must be a string` };
|
|
2235
|
+
}
|
|
2236
|
+
return { argv: [`--${flag.name}`, String(value)] };
|
|
2237
|
+
}
|
|
2238
|
+
function commandsFromBlock(help) {
|
|
2239
|
+
let section = "none";
|
|
2240
|
+
const commands = [];
|
|
2241
|
+
const globalFlags = [];
|
|
2242
|
+
const scoped = [];
|
|
2243
|
+
for (const rawLine of help.split("\n")) {
|
|
2244
|
+
const line = rawLine.replace(/\t/g, " ");
|
|
2245
|
+
const header = classifyHeader(line);
|
|
2246
|
+
if (header) {
|
|
2247
|
+
section = header.kind === "other" ? "none" : header.kind;
|
|
2248
|
+
if (header.kind === "scoped" && header.name) scoped.push({ name: header.name, flags: [] });
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
pushSectionLine(section, line, commands, globalFlags, scoped);
|
|
2252
|
+
}
|
|
2253
|
+
return { commands, globalFlags, scoped };
|
|
2254
|
+
}
|
|
2255
|
+
function pushSectionLine(section, line, commands, globalFlags, scoped) {
|
|
2256
|
+
if (section === "commands") {
|
|
2257
|
+
const command = parseCommandLine(line);
|
|
2258
|
+
if (command) commands.push(command);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
const flag = parseFlagLine(line);
|
|
2262
|
+
if (!flag) return;
|
|
2263
|
+
if (section === "options") globalFlags.push(flag);
|
|
2264
|
+
if (section === "scoped") scoped[scoped.length - 1]?.flags.push(flag);
|
|
2265
|
+
}
|
|
2266
|
+
function classifyHeader(line) {
|
|
2267
|
+
const text = line.trim();
|
|
2268
|
+
if (/^commands:?$/i.test(text)) return { kind: "commands" };
|
|
2269
|
+
if (/^options:?$/i.test(text)) return { kind: "options" };
|
|
2270
|
+
const flags = /^([A-Za-z][\w-]*)\s+flags:?$/i.exec(text);
|
|
2271
|
+
if (flags?.[1]) return { kind: "scoped", name: flags[1].toLowerCase() };
|
|
2272
|
+
if (/^[A-Z][A-Z0-9 ]+:$/.test(text)) return { kind: "other" };
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
function parseCommandLine(line) {
|
|
2276
|
+
if (!/^\s{2,}\S/.test(line)) return null;
|
|
2277
|
+
const parts = line.trim().split(/\s{2,}/);
|
|
2278
|
+
if (parts.length < 2) return null;
|
|
2279
|
+
const tokens = tokenize(parts[0] ?? "");
|
|
2280
|
+
const name = tokens[0] ?? "";
|
|
2281
|
+
if (!NAME_RE.test(name) || name.startsWith("-")) return null;
|
|
2282
|
+
const spec = emptySpec();
|
|
2283
|
+
for (const token of tokens.slice(1)) parseUsageToken(token, spec);
|
|
2284
|
+
return {
|
|
2285
|
+
path: [name],
|
|
2286
|
+
description: parts.slice(1).join(" ").trim(),
|
|
2287
|
+
positionals: spec.positionals,
|
|
2288
|
+
flags: spec.flags
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function usageProgramName(help) {
|
|
2292
|
+
for (const raw of help.split("\n")) {
|
|
2293
|
+
if (!/^[\s>*]*usage:/i.test(raw)) continue;
|
|
2294
|
+
const tokens = tokenize(raw);
|
|
2295
|
+
const name = tokens[0];
|
|
2296
|
+
if (name && NAME_RE.test(name)) return name;
|
|
2297
|
+
}
|
|
2298
|
+
return void 0;
|
|
2299
|
+
}
|
|
2300
|
+
function usageCommands(help, program) {
|
|
2301
|
+
const commands = [];
|
|
2302
|
+
for (const raw of help.split("\n")) {
|
|
2303
|
+
const command = usageCommand(raw, program);
|
|
2304
|
+
if (command) commands.push(command);
|
|
2305
|
+
}
|
|
2306
|
+
return commands;
|
|
2307
|
+
}
|
|
2308
|
+
function isCommandSlot(token) {
|
|
2309
|
+
const inner = token.replace(/^\[|\]$/g, "").replace(/^<|>$/g, "").toLowerCase();
|
|
2310
|
+
return inner === "command" || inner === "cmd" || inner === "subcommand" || inner === "verb";
|
|
2311
|
+
}
|
|
2312
|
+
function usageCommand(raw, program) {
|
|
2313
|
+
const line = raw.trim();
|
|
2314
|
+
const tokens = tokenize(line);
|
|
2315
|
+
if (tokens.length < 2) return null;
|
|
2316
|
+
const isUsage = /^usage:/i.test(line);
|
|
2317
|
+
if (!isUsage && tokens[0] !== program) return null;
|
|
2318
|
+
const path = [];
|
|
2319
|
+
let index = 1;
|
|
2320
|
+
while (index < tokens.length && path.length < 2 && NAME_RE.test(tokens[index] ?? "")) {
|
|
2321
|
+
path.push(tokens[index] ?? "");
|
|
2322
|
+
index++;
|
|
2323
|
+
}
|
|
2324
|
+
if (path.length === 0) return null;
|
|
2325
|
+
if (isCommandSlot(tokens[index] ?? "")) return null;
|
|
2326
|
+
const spec = emptySpec();
|
|
2327
|
+
for (const token of tokens.slice(index)) parseUsageToken(token, spec);
|
|
2328
|
+
return { path, description: "", positionals: spec.positionals, flags: spec.flags };
|
|
2329
|
+
}
|
|
2330
|
+
function emptySpec() {
|
|
2331
|
+
return { positionals: [], flags: [] };
|
|
2332
|
+
}
|
|
2333
|
+
function parseUsageToken(token, into) {
|
|
2334
|
+
const optional = token.startsWith("[") && token.endsWith("]");
|
|
2335
|
+
const inner = optional ? token.slice(1, -1).trim() : token;
|
|
2336
|
+
if (inner.startsWith("--")) {
|
|
2337
|
+
const flag = parseFlagToken(inner);
|
|
2338
|
+
if (flag) into.flags.push(flag);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
if (inner.startsWith("<") && inner.endsWith(">")) {
|
|
2342
|
+
into.positionals.push({
|
|
2343
|
+
name: cleanName(inner.slice(1, -1)),
|
|
2344
|
+
required: !optional,
|
|
2345
|
+
description: ""
|
|
2346
|
+
});
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (NAME_RE.test(inner)) {
|
|
2350
|
+
into.positionals.push({ name: cleanName(inner), required: !optional, description: "" });
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
function parseFlagToken(inner) {
|
|
2354
|
+
const match = /^--([A-Za-z][\w-]*)(?:(?:=|\s+)(\S+))?$/.exec(inner.trim());
|
|
2355
|
+
if (!match?.[1]) return null;
|
|
2356
|
+
const value = match[2] ?? "";
|
|
2357
|
+
if (!value) return { name: match[1], type: "boolean", description: "" };
|
|
2358
|
+
return { name: match[1], type: value.includes("...") ? "string[]" : "string", description: "" };
|
|
2359
|
+
}
|
|
2360
|
+
function parseFlagLine(line) {
|
|
2361
|
+
const trimmed = line.trim();
|
|
2362
|
+
if (!trimmed.includes("--")) return null;
|
|
2363
|
+
const match = /--([A-Za-z][\w-]*)(?:=(\S+)|(?:\s+(<[^>]+>|\[[^\]]+\]|[A-Z][A-Z0-9_-]+)))?/.exec(
|
|
2364
|
+
trimmed
|
|
2365
|
+
);
|
|
2366
|
+
if (!match?.[1]) return null;
|
|
2367
|
+
const value = match[2] || match[3] || "";
|
|
2368
|
+
const repeatable = /\brepeat|\.\.\.|one or more/i.test(trimmed);
|
|
2369
|
+
const description = flagDescription(trimmed);
|
|
2370
|
+
if (!value && !repeatable) return { name: match[1], type: "boolean", description };
|
|
2371
|
+
return { name: match[1], type: repeatable ? "string[]" : "string", description };
|
|
2372
|
+
}
|
|
2373
|
+
function flagDescription(line) {
|
|
2374
|
+
const parts = line.trim().split(/\s{2,}/);
|
|
2375
|
+
return parts.length > 1 ? parts.slice(1).join(" ").trim() : "";
|
|
2376
|
+
}
|
|
2377
|
+
function tokenize(line) {
|
|
2378
|
+
const source = line.trim().replace(/^usage:\s*/i, "");
|
|
2379
|
+
const tokens = [];
|
|
2380
|
+
let index = 0;
|
|
2381
|
+
while (index < source.length) {
|
|
2382
|
+
if (source[index] === " ") {
|
|
2383
|
+
index++;
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const closer = source[index] === "[" ? "]" : source[index] === "<" ? ">" : "";
|
|
2387
|
+
if (closer) {
|
|
2388
|
+
const end = source.indexOf(closer, index);
|
|
2389
|
+
if (end === -1) {
|
|
2390
|
+
tokens.push(source.slice(index));
|
|
2391
|
+
break;
|
|
2392
|
+
}
|
|
2393
|
+
tokens.push(source.slice(index, end + 1));
|
|
2394
|
+
index = end + 1;
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
const next = source.indexOf(" ", index);
|
|
2398
|
+
if (next === -1) {
|
|
2399
|
+
tokens.push(source.slice(index));
|
|
2400
|
+
break;
|
|
2401
|
+
}
|
|
2402
|
+
tokens.push(source.slice(index, next));
|
|
2403
|
+
index = next;
|
|
2404
|
+
}
|
|
2405
|
+
return tokens;
|
|
2406
|
+
}
|
|
2407
|
+
function mergeCommandLists(primary, extra) {
|
|
2408
|
+
const merged = primary.map((command) => ({ ...command }));
|
|
2409
|
+
for (const command of extra) {
|
|
2410
|
+
const existing = merged.find((item) => samePath(item.path, command.path));
|
|
2411
|
+
if (!existing) {
|
|
2412
|
+
merged.push(command);
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
if (existing.positionals.length === 0) existing.positionals = command.positionals;
|
|
2416
|
+
if (!existing.description) existing.description = command.description;
|
|
2417
|
+
existing.flags = mergeFlags(existing.flags, command.flags);
|
|
2418
|
+
}
|
|
2419
|
+
return merged;
|
|
2420
|
+
}
|
|
2421
|
+
function applyScopedFlags(commands, scoped) {
|
|
2422
|
+
return commands.map((command) => {
|
|
2423
|
+
const extra = scoped.find((scope) => scope.name === (command.path[0] ?? "").toLowerCase());
|
|
2424
|
+
if (!extra) return command;
|
|
2425
|
+
return { ...command, flags: mergeFlags(command.flags, extra.flags) };
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
function mergeFlags(left, right) {
|
|
2429
|
+
const merged = [...left];
|
|
2430
|
+
for (const flag of right) {
|
|
2431
|
+
if (!merged.some((existing) => existing.name === flag.name)) merged.push(flag);
|
|
2432
|
+
}
|
|
2433
|
+
return merged;
|
|
2434
|
+
}
|
|
2435
|
+
function samePath(left, right) {
|
|
2436
|
+
return left.length === right.length && left.every((part, index) => part === right[index]);
|
|
2437
|
+
}
|
|
2438
|
+
function childPath(group, child) {
|
|
2439
|
+
if (child.path[0] === group) return child.path.slice(0, 2);
|
|
2440
|
+
return [group, child.path[0] ?? ""].filter((part) => part.length > 0).slice(0, 2);
|
|
2441
|
+
}
|
|
2442
|
+
function cleanName(raw) {
|
|
2443
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_-]+/g, "").replace(/^-+/, "");
|
|
2444
|
+
return cleaned || "arg";
|
|
2445
|
+
}
|
|
2446
|
+
function readMcpCommand(value) {
|
|
2447
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2448
|
+
const name = ownString(value, "name");
|
|
2449
|
+
if (!name) return null;
|
|
2450
|
+
const path = name.split(/\s+/).filter((part) => NAME_RE.test(part)).slice(0, 2);
|
|
2451
|
+
if (path.length === 0) return null;
|
|
2452
|
+
return {
|
|
2453
|
+
path,
|
|
2454
|
+
description: ownString(value, "description") ?? "",
|
|
2455
|
+
positionals: readMcpPositionals(Object.getOwnPropertyDescriptor(value, "positionals")?.value),
|
|
2456
|
+
flags: readMcpFlags(Object.getOwnPropertyDescriptor(value, "flags")?.value)
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function readMcpPositionals(value) {
|
|
2460
|
+
if (!Array.isArray(value)) return [];
|
|
2461
|
+
const positionals = [];
|
|
2462
|
+
for (const entry of value) {
|
|
2463
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
2464
|
+
const name = ownString(entry, "name");
|
|
2465
|
+
if (!name) continue;
|
|
2466
|
+
const required = Object.getOwnPropertyDescriptor(entry, "required")?.value;
|
|
2467
|
+
positionals.push({ name, required: required !== false, description: "" });
|
|
2468
|
+
}
|
|
2469
|
+
return positionals;
|
|
2470
|
+
}
|
|
2471
|
+
function readMcpFlags(value) {
|
|
2472
|
+
if (!Array.isArray(value)) return [];
|
|
2473
|
+
const flags = [];
|
|
2474
|
+
for (const entry of value) {
|
|
2475
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
2476
|
+
const name = ownString(entry, "name");
|
|
2477
|
+
const type = ownString(entry, "type");
|
|
2478
|
+
if (!name || type !== "boolean" && type !== "string" && type !== "string[]") continue;
|
|
2479
|
+
flags.push({ name, type, description: "" });
|
|
2480
|
+
}
|
|
2481
|
+
return flags;
|
|
2482
|
+
}
|
|
2483
|
+
function ownString(value, key) {
|
|
2484
|
+
const found = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
2485
|
+
return typeof found === "string" && found.length > 0 ? found : void 0;
|
|
2486
|
+
}
|
|
2487
|
+
function objectSchema(properties, required) {
|
|
2488
|
+
return {
|
|
2489
|
+
type: "object",
|
|
2490
|
+
properties,
|
|
2491
|
+
additionalProperties: false,
|
|
2492
|
+
...required && required.length > 0 ? { required } : {}
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
function invokeSchema() {
|
|
2496
|
+
return objectSchema(
|
|
2497
|
+
{
|
|
2498
|
+
argv: {
|
|
2499
|
+
type: "array",
|
|
2500
|
+
items: { type: "string" },
|
|
2501
|
+
description: "Arguments, one element per argument. Never a shell string."
|
|
2502
|
+
},
|
|
2503
|
+
stdin: { type: "string", description: "Optional standard input." }
|
|
2504
|
+
},
|
|
2505
|
+
["argv"]
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
function commandSchema(command) {
|
|
2509
|
+
const properties = {};
|
|
2510
|
+
const required = [];
|
|
2511
|
+
for (const positional of command.positionals) {
|
|
2512
|
+
properties[positional.name] = {
|
|
2513
|
+
type: "string",
|
|
2514
|
+
description: positional.description || positional.name
|
|
2515
|
+
};
|
|
2516
|
+
if (positional.required) required.push(positional.name);
|
|
2517
|
+
}
|
|
2518
|
+
for (const flag of command.flags) {
|
|
2519
|
+
properties[flag.name] = flagProperty(flag);
|
|
2520
|
+
}
|
|
2521
|
+
properties.stdin = { type: "string", description: "Optional standard input." };
|
|
2522
|
+
properties.passthrough = {
|
|
2523
|
+
type: "array",
|
|
2524
|
+
items: { type: "string" },
|
|
2525
|
+
description: "Extra arguments appended as-is."
|
|
2526
|
+
};
|
|
2527
|
+
return objectSchema(properties, required);
|
|
2528
|
+
}
|
|
2529
|
+
function flagProperty(flag) {
|
|
2530
|
+
if (flag.type === "boolean")
|
|
2531
|
+
return { type: "boolean", description: flag.description || flag.name };
|
|
2532
|
+
if (flag.type === "string[]") {
|
|
2533
|
+
return { type: "array", items: { type: "string" }, description: flag.description || flag.name };
|
|
2534
|
+
}
|
|
2535
|
+
return { type: "string", description: flag.description || flag.name };
|
|
2536
|
+
}
|
|
2537
|
+
function readArgument(key, value) {
|
|
2538
|
+
if (key === "stdin") {
|
|
2539
|
+
return typeof value === "string" ? { stdin: value } : { error: "stdin must be a string" };
|
|
2540
|
+
}
|
|
2541
|
+
if (key === "passthrough") return readStringArray(value, "passthrough");
|
|
2542
|
+
if (key === "argv") {
|
|
2543
|
+
const read = readStringArray(value, "argv");
|
|
2544
|
+
return read.passthrough ? { argv: read.passthrough } : read;
|
|
2545
|
+
}
|
|
2546
|
+
if (!isArgValue(value)) return { error: `${key} has an unsupported type` };
|
|
2547
|
+
return { field: { name: key, value } };
|
|
2548
|
+
}
|
|
2549
|
+
function readStringArray(value, label) {
|
|
2550
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
2551
|
+
return { error: `${label} must be an array of strings` };
|
|
2552
|
+
}
|
|
2553
|
+
return { passthrough: value };
|
|
2554
|
+
}
|
|
2555
|
+
function isArgValue(value) {
|
|
2556
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number")
|
|
2557
|
+
return true;
|
|
2558
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2559
|
+
}
|
|
2560
|
+
function parseToolTemplate(spec) {
|
|
2561
|
+
const colonIdx = spec.indexOf(":");
|
|
2562
|
+
if (colonIdx <= 0) return null;
|
|
2563
|
+
const name = spec.slice(0, colonIdx).trim();
|
|
2564
|
+
const template = spec.slice(colonIdx + 1).trim();
|
|
2565
|
+
if (!name || !template) return null;
|
|
2566
|
+
const placeholders = [];
|
|
2567
|
+
const re = /\{(\w+)\}/g;
|
|
2568
|
+
let m;
|
|
2569
|
+
while ((m = re.exec(template)) !== null) {
|
|
2570
|
+
if (!placeholders.includes(m[1])) placeholders.push(m[1]);
|
|
2571
|
+
}
|
|
2572
|
+
const properties = {};
|
|
2573
|
+
for (const ph of placeholders) {
|
|
2574
|
+
properties[ph] = { type: "string", description: ph };
|
|
2575
|
+
}
|
|
2576
|
+
return {
|
|
2577
|
+
name,
|
|
2578
|
+
description: template,
|
|
2579
|
+
inputSchema: {
|
|
2580
|
+
type: "object",
|
|
2581
|
+
properties,
|
|
2582
|
+
required: placeholders,
|
|
2583
|
+
additionalProperties: false
|
|
2584
|
+
},
|
|
2585
|
+
cli: "",
|
|
2586
|
+
kind: "command",
|
|
2587
|
+
commandPath: []
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
function templateToArgv(template, args) {
|
|
2591
|
+
let expanded = template;
|
|
2592
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2593
|
+
expanded = expanded.replaceAll(`{${key}}`, value);
|
|
2594
|
+
}
|
|
2595
|
+
return expanded.split(/\s+/).filter(Boolean);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// src/serve/http.ts
|
|
2599
|
+
var encoder = new TextEncoder();
|
|
2600
|
+
var decoder = new TextDecoder();
|
|
2601
|
+
var MAX_HEADER_SIZE = 16384;
|
|
2602
|
+
var MAX_BODY_SIZE = 10485760;
|
|
2603
|
+
async function readUntil(reader, buf, bufLen, sentinel, maxSize) {
|
|
2604
|
+
for (; ; ) {
|
|
2605
|
+
const text = decoder.decode(new Uint8Array(buf.buffer, buf.byteOffset, bufLen));
|
|
2606
|
+
const pos = text.indexOf(sentinel);
|
|
2607
|
+
if (pos >= 0) return { buf, bufLen, pos };
|
|
2608
|
+
if (bufLen >= maxSize) return null;
|
|
2609
|
+
const { done, value } = await reader.read();
|
|
2610
|
+
if (done) return null;
|
|
2611
|
+
if (bufLen + value.length > buf.length) {
|
|
2612
|
+
const newBuf = new Uint8Array(Math.max(buf.length * 2, bufLen + value.length));
|
|
2613
|
+
newBuf.set(new Uint8Array(buf.buffer, buf.byteOffset, bufLen));
|
|
2614
|
+
buf = newBuf;
|
|
2615
|
+
}
|
|
2616
|
+
buf.set(value, bufLen);
|
|
2617
|
+
bufLen += value.length;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
async function readExact(reader, buf, bufLen, needed) {
|
|
2621
|
+
while (bufLen < needed) {
|
|
2622
|
+
const { done, value } = await reader.read();
|
|
2623
|
+
if (done) return null;
|
|
2624
|
+
if (bufLen + value.length > buf.length) {
|
|
2625
|
+
const newBuf = new Uint8Array(Math.max(buf.length * 2, bufLen + value.length));
|
|
2626
|
+
newBuf.set(new Uint8Array(buf.buffer, buf.byteOffset, bufLen));
|
|
2627
|
+
buf = newBuf;
|
|
2628
|
+
}
|
|
2629
|
+
buf.set(value, bufLen);
|
|
2630
|
+
bufLen += value.length;
|
|
2631
|
+
}
|
|
2632
|
+
return { buf, bufLen };
|
|
2633
|
+
}
|
|
2634
|
+
function parseRequestLine(buf, endPos) {
|
|
2635
|
+
const headText = decoder.decode(new Uint8Array(buf.buffer, buf.byteOffset, endPos));
|
|
2636
|
+
const lines = headText.split("\r\n");
|
|
2637
|
+
const requestLine = lines[0];
|
|
2638
|
+
if (!requestLine) return null;
|
|
2639
|
+
const parts = requestLine.split(" ");
|
|
2640
|
+
if (parts.length < 2) return null;
|
|
2641
|
+
const method = parts[0];
|
|
2642
|
+
const target = parts[1];
|
|
2643
|
+
const headers = {};
|
|
2644
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2645
|
+
const line = lines[i];
|
|
2646
|
+
if (!line) continue;
|
|
2647
|
+
const colonIdx = line.indexOf(":");
|
|
2648
|
+
if (colonIdx <= 0) continue;
|
|
2649
|
+
headers[line.slice(0, colonIdx).trim().toLowerCase()] = line.slice(colonIdx + 1).trim();
|
|
2650
|
+
}
|
|
2651
|
+
return { method, target, headers, headerEnd: endPos + 4 };
|
|
2652
|
+
}
|
|
2653
|
+
async function handleConnection(readable, writable, handler) {
|
|
2654
|
+
const reader = readable.getReader();
|
|
2655
|
+
const writer = writable.getWriter();
|
|
2656
|
+
let buf = new Uint8Array(4096);
|
|
2657
|
+
let bufLen = 0;
|
|
2658
|
+
try {
|
|
2659
|
+
for (; ; ) {
|
|
2660
|
+
const headerResult = await readUntil(reader, buf, bufLen, "\r\n\r\n", MAX_HEADER_SIZE);
|
|
2661
|
+
if (!headerResult) break;
|
|
2662
|
+
buf = headerResult.buf;
|
|
2663
|
+
bufLen = headerResult.bufLen;
|
|
2664
|
+
const parsed = parseRequestLine(buf, headerResult.pos);
|
|
2665
|
+
if (!parsed) {
|
|
2666
|
+
await writeResponse(writer, {
|
|
2667
|
+
status: 400,
|
|
2668
|
+
reason: "Bad Request",
|
|
2669
|
+
headers: { "content-type": "text/plain" },
|
|
2670
|
+
body: "Bad Request"
|
|
2671
|
+
});
|
|
2672
|
+
break;
|
|
2673
|
+
}
|
|
2674
|
+
const contentLength = Number(parsed.headers["content-length"] ?? 0);
|
|
2675
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
2676
|
+
await writeResponse(writer, {
|
|
2677
|
+
status: 413,
|
|
2678
|
+
reason: "Payload Too Large",
|
|
2679
|
+
headers: { "content-type": "text/plain" },
|
|
2680
|
+
body: "Payload Too Large"
|
|
2681
|
+
});
|
|
2682
|
+
break;
|
|
2683
|
+
}
|
|
2684
|
+
const bodyStart = parsed.headerEnd;
|
|
2685
|
+
const totalNeeded = bodyStart + contentLength;
|
|
2686
|
+
const bodyResult = await readExact(reader, buf, bufLen, totalNeeded);
|
|
2687
|
+
if (!bodyResult && contentLength > 0) break;
|
|
2688
|
+
if (bodyResult) {
|
|
2689
|
+
buf = bodyResult.buf;
|
|
2690
|
+
bufLen = bodyResult.bufLen;
|
|
2691
|
+
}
|
|
2692
|
+
const bodyBytes = buf.slice(bodyStart, bodyStart + contentLength);
|
|
2693
|
+
const body = decoder.decode(bodyBytes);
|
|
2694
|
+
const request = {
|
|
2695
|
+
method: parsed.method,
|
|
2696
|
+
target: parsed.target,
|
|
2697
|
+
headers: parsed.headers,
|
|
2698
|
+
body
|
|
2699
|
+
};
|
|
2700
|
+
const response = await handler(request);
|
|
2701
|
+
await writeResponse(writer, response);
|
|
2702
|
+
const connectionHeader = (parsed.headers.connection ?? "").toLowerCase();
|
|
2703
|
+
if (connectionHeader === "close") break;
|
|
2704
|
+
const consumed = bodyStart + contentLength;
|
|
2705
|
+
const remaining = bufLen - consumed;
|
|
2706
|
+
if (remaining > 0) {
|
|
2707
|
+
const newBuf = new Uint8Array(Math.max(4096, remaining));
|
|
2708
|
+
newBuf.set(new Uint8Array(buf.buffer, buf.byteOffset + consumed, remaining));
|
|
2709
|
+
buf = newBuf;
|
|
2710
|
+
bufLen = remaining;
|
|
2711
|
+
} else {
|
|
2712
|
+
buf = new Uint8Array(4096);
|
|
2713
|
+
bufLen = 0;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
} finally {
|
|
2717
|
+
reader.releaseLock();
|
|
2718
|
+
try {
|
|
2719
|
+
await writer.close();
|
|
2720
|
+
} catch {
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
async function writeResponse(writer, response) {
|
|
2725
|
+
const bodyLength = new TextEncoder().encode(response.body).length;
|
|
2726
|
+
const headers = { ...response.headers, "content-length": String(bodyLength) };
|
|
2727
|
+
const lines = [`HTTP/1.1 ${response.status} ${response.reason}`];
|
|
2728
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2729
|
+
lines.push(`${name}: ${value}`);
|
|
2730
|
+
}
|
|
2731
|
+
lines.push("", "");
|
|
2732
|
+
const headBytes = encoder.encode(lines.join("\r\n"));
|
|
2733
|
+
await writer.write(headBytes);
|
|
2734
|
+
if (response.body.length > 0) {
|
|
2735
|
+
const bodyBytes = encoder.encode(response.body);
|
|
2736
|
+
await writer.write(bodyBytes);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
// src/serve/jsonrpc.ts
|
|
2741
|
+
var PROTOCOL_VERSIONS = ["2026-07-28", "2025-11-25", "2025-06-18"];
|
|
2742
|
+
var OUTPUT_CAP = 1048576;
|
|
2743
|
+
var JSON_CT = "application/json; charset=utf-8";
|
|
2744
|
+
async function handleJsonRpc(raw, tools, call, serverName, serverVersion) {
|
|
2745
|
+
let parsed;
|
|
2746
|
+
try {
|
|
2747
|
+
parsed = JSON.parse(raw);
|
|
2748
|
+
} catch {
|
|
2749
|
+
return rpcError(null, -32700, "Parse error");
|
|
2750
|
+
}
|
|
2751
|
+
if (Array.isArray(parsed)) return rpcError(null, -32600, "Batch requests are not supported");
|
|
2752
|
+
if (!parsed || typeof parsed !== "object") return rpcError(null, -32600, "Invalid request");
|
|
2753
|
+
const method = readString(parsed, "method");
|
|
2754
|
+
if (!method) return rpcError(readId(parsed), -32600, "Invalid request");
|
|
2755
|
+
if (!hasOwn(parsed, "id")) return { status: 202, contentType: "text/plain", body: "" };
|
|
2756
|
+
const id = readId(parsed);
|
|
2757
|
+
const params = Object.getOwnPropertyDescriptor(parsed, "params")?.value;
|
|
2758
|
+
if (method === "initialize")
|
|
2759
|
+
return rpcOk(id, initializeResult(params, serverName, serverVersion));
|
|
2760
|
+
if (method === "server/discover") return rpcOk(id, discoverResult(serverName, serverVersion));
|
|
2761
|
+
if (method === "tools/list") return rpcOk(id, { tools });
|
|
2762
|
+
if (method === "ping") return rpcOk(id, {});
|
|
2763
|
+
if (method === "tools/call") return toolCall(id, params, tools, call);
|
|
2764
|
+
return rpcError(id, -32601, `Method not found: ${method}`);
|
|
2765
|
+
}
|
|
2766
|
+
async function toolCall(id, params, tools, call) {
|
|
2767
|
+
const name = params && typeof params === "object" ? readString(params, "name") : void 0;
|
|
2768
|
+
if (!name) return rpcError(id, -32602, "tools/call requires a name");
|
|
2769
|
+
if (!tools.some((tool) => tool.name === name))
|
|
2770
|
+
return rpcError(id, -32602, `Unknown tool: ${name}`);
|
|
2771
|
+
const args = params && typeof params === "object" ? Object.getOwnPropertyDescriptor(params, "arguments")?.value : void 0;
|
|
2772
|
+
try {
|
|
2773
|
+
const result = await call(name, args);
|
|
2774
|
+
return rpcOk(id, toolResult(result));
|
|
2775
|
+
} catch (err) {
|
|
2776
|
+
const message = err instanceof Error ? err.message : "tool call failed";
|
|
2777
|
+
return rpcError(id, -32e3, message);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
function toolResult(result) {
|
|
2781
|
+
const stdout = cap(result.stdout);
|
|
2782
|
+
const stderr = cap(result.stderr);
|
|
2783
|
+
const text = stderr ? stdout ? `${stdout}
|
|
2784
|
+
${stderr}` : stderr : stdout;
|
|
2785
|
+
return {
|
|
2786
|
+
content: [{ type: "text", text }],
|
|
2787
|
+
isError: result.exitCode !== 0,
|
|
2788
|
+
structuredContent: { stdout, stderr, exitCode: result.exitCode }
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
function initializeResult(params, serverName, serverVersion) {
|
|
2792
|
+
const requested = params && typeof params === "object" ? readString(params, "protocolVersion") : void 0;
|
|
2793
|
+
const protocolVersion = PROTOCOL_VERSIONS.find((version) => version === requested) ?? PROTOCOL_VERSIONS[0];
|
|
2794
|
+
return {
|
|
2795
|
+
protocolVersion,
|
|
2796
|
+
capabilities: { tools: { listChanged: false } },
|
|
2797
|
+
serverInfo: { name: serverName, version: serverVersion }
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
function discoverResult(serverName, serverVersion) {
|
|
2801
|
+
return {
|
|
2802
|
+
protocolVersion: PROTOCOL_VERSIONS[0],
|
|
2803
|
+
supportedVersions: PROTOCOL_VERSIONS,
|
|
2804
|
+
capabilities: { tools: { listChanged: false } },
|
|
2805
|
+
serverInfo: { name: serverName, version: serverVersion }
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
function cap(value) {
|
|
2809
|
+
if (value.length <= OUTPUT_CAP) return value;
|
|
2810
|
+
const half = Math.floor(OUTPUT_CAP / 2);
|
|
2811
|
+
const cut = value.length - OUTPUT_CAP;
|
|
2812
|
+
return `${value.slice(0, half)}
|
|
2813
|
+
\u2026 ${cut} characters cut \u2026
|
|
2814
|
+
${value.slice(value.length - half)}`;
|
|
2815
|
+
}
|
|
2816
|
+
function rpcOk(id, result) {
|
|
2817
|
+
return {
|
|
2818
|
+
status: 200,
|
|
2819
|
+
contentType: JSON_CT,
|
|
2820
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, result })
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function rpcError(id, code, message) {
|
|
2824
|
+
return {
|
|
2825
|
+
status: 200,
|
|
2826
|
+
contentType: JSON_CT,
|
|
2827
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function readId(value) {
|
|
2831
|
+
const id = Object.getOwnPropertyDescriptor(value, "id")?.value;
|
|
2832
|
+
if (typeof id === "string" || typeof id === "number" || id === null) return id;
|
|
2833
|
+
return null;
|
|
2834
|
+
}
|
|
2835
|
+
function readString(value, key) {
|
|
2836
|
+
const found = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
2837
|
+
return typeof found === "string" ? found : void 0;
|
|
2838
|
+
}
|
|
2839
|
+
function hasOwn(value, key) {
|
|
2840
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// src/serve/origin.ts
|
|
2844
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
2845
|
+
function isLoopbackOrigin(origin) {
|
|
2846
|
+
try {
|
|
2847
|
+
const url = new URL(origin);
|
|
2848
|
+
const hostname = url.hostname;
|
|
2849
|
+
if (LOOPBACK_HOSTS.has(hostname)) return true;
|
|
2850
|
+
if (hostname === "::1") return true;
|
|
2851
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
2852
|
+
return false;
|
|
2853
|
+
} catch {
|
|
2854
|
+
return false;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
function isLoopbackHost(host) {
|
|
2858
|
+
const hostname = host.replace(/:\d+$/, "");
|
|
2859
|
+
if (LOOPBACK_HOSTS.has(hostname)) return true;
|
|
2860
|
+
if (hostname === "::1") return true;
|
|
2861
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
function checkOrigin(origin, allowedOrigins, boundLoopback) {
|
|
2865
|
+
if (origin === void 0) return { ok: true };
|
|
2866
|
+
if (allowedOrigins.has(origin)) return { ok: true };
|
|
2867
|
+
if (isLoopbackOrigin(origin)) return { ok: true };
|
|
2868
|
+
if (!boundLoopback) return { ok: true };
|
|
2869
|
+
return { ok: false, reason: `origin ${origin} is not allowed` };
|
|
2870
|
+
}
|
|
2871
|
+
function checkHost(host, boundLoopback) {
|
|
2872
|
+
if (!boundLoopback) return { ok: true };
|
|
2873
|
+
if (!host) return { ok: false, reason: "missing Host header" };
|
|
2874
|
+
if (isLoopbackHost(host)) return { ok: true };
|
|
2875
|
+
return { ok: false, reason: `host ${host} is not loopback` };
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// src/serve/runtime.ts
|
|
2879
|
+
var CALL_TIMEOUT_MS = 12e4;
|
|
2880
|
+
var MAX_OUTPUT_DEFAULT = 1048576;
|
|
2881
|
+
var ALLOWED_ENV_KEYS = /* @__PURE__ */ new Set(["PATH", "HOME", "LANG"]);
|
|
2882
|
+
function parseHttpFlag(args, i) {
|
|
2883
|
+
const next = args[i + 1];
|
|
2884
|
+
if (!next || next.startsWith("-")) return { host: "127.0.0.1", port: 0, next: i };
|
|
2885
|
+
const colonIdx = next.lastIndexOf(":");
|
|
2886
|
+
if (colonIdx > 0 && colonIdx < next.length - 1) {
|
|
2887
|
+
return { host: next.slice(0, colonIdx), port: Number(next.slice(colonIdx + 1)), next: i + 1 };
|
|
2888
|
+
}
|
|
2889
|
+
return { host: "127.0.0.1", port: Number(next), next: i + 1 };
|
|
2890
|
+
}
|
|
2891
|
+
function parseCommandTarget(arg) {
|
|
2892
|
+
const eqIdx = arg.indexOf("=");
|
|
2893
|
+
if (eqIdx > 0 && !arg.slice(0, eqIdx).includes("/")) {
|
|
2894
|
+
return { name: arg.slice(0, eqIdx), path: arg.slice(eqIdx + 1) };
|
|
2895
|
+
}
|
|
2896
|
+
return { path: arg };
|
|
2897
|
+
}
|
|
2898
|
+
function parseSingleFlag(args, i, opts) {
|
|
2899
|
+
const arg = args[i];
|
|
2900
|
+
if (arg === "--stdio") {
|
|
2901
|
+
opts.transport = "stdio";
|
|
2902
|
+
} else if (arg === "--http") {
|
|
2903
|
+
opts.transport = "http";
|
|
2904
|
+
const parsed = parseHttpFlag(args, i);
|
|
2905
|
+
opts.host = parsed.host;
|
|
2906
|
+
opts.port = parsed.port;
|
|
2907
|
+
return parsed.next;
|
|
2908
|
+
} else if (arg === "--bearer" && args[i + 1]) {
|
|
2909
|
+
opts.bearer = args[++i];
|
|
2910
|
+
} else if (arg === "--allow-origin" && args[i + 1]) {
|
|
2911
|
+
opts.allowOrigins.push(args[++i]);
|
|
2912
|
+
} else if (arg === "--env" && args[i + 1]) {
|
|
2913
|
+
opts.envAllow.push(args[++i]);
|
|
2914
|
+
} else if (arg === "--tool" && args[i + 1]) {
|
|
2915
|
+
opts.toolTemplates.push(args[++i]);
|
|
2916
|
+
} else if (arg === "--call-timeout" && args[i + 1]) {
|
|
2917
|
+
opts.callTimeout = Number(args[++i]) * 1e3;
|
|
2918
|
+
} else if (arg === "--concurrency" && args[i + 1]) {
|
|
2919
|
+
opts.concurrency = Number(args[++i]);
|
|
2920
|
+
} else if (arg === "--max-output" && args[i + 1]) {
|
|
2921
|
+
opts.maxOutput = Number(args[++i]);
|
|
2922
|
+
} else if (arg === "--cwd" && args[i + 1]) {
|
|
2923
|
+
opts.cwd = args[++i];
|
|
2924
|
+
} else if (arg === "--json") {
|
|
2925
|
+
opts.json = true;
|
|
2926
|
+
} else if (!arg.startsWith("-")) {
|
|
2927
|
+
opts.commands.push(parseCommandTarget(arg));
|
|
2928
|
+
}
|
|
2929
|
+
return i;
|
|
2930
|
+
}
|
|
2931
|
+
function parseServeFlags(args) {
|
|
2932
|
+
const opts = {
|
|
2933
|
+
transport: "stdio",
|
|
2934
|
+
host: "127.0.0.1",
|
|
2935
|
+
port: 0,
|
|
2936
|
+
bearer: void 0,
|
|
2937
|
+
allowOrigins: [],
|
|
2938
|
+
envAllow: [],
|
|
2939
|
+
toolTemplates: [],
|
|
2940
|
+
callTimeout: CALL_TIMEOUT_MS,
|
|
2941
|
+
concurrency: 1,
|
|
2942
|
+
maxOutput: MAX_OUTPUT_DEFAULT,
|
|
2943
|
+
cwd: void 0,
|
|
2944
|
+
json: false,
|
|
2945
|
+
commands: []
|
|
2946
|
+
};
|
|
2947
|
+
for (let i = 0; i < args.length; i++) {
|
|
2948
|
+
i = parseSingleFlag(args, i, opts);
|
|
2949
|
+
}
|
|
2950
|
+
return opts;
|
|
2951
|
+
}
|
|
2952
|
+
function expandBearerToken(template, env) {
|
|
2953
|
+
if (!containsPlaceholders(template)) return void 0;
|
|
2954
|
+
return expandPlaceholdersStrict(template, env);
|
|
2955
|
+
}
|
|
2956
|
+
function constantTimeEqual(a, b) {
|
|
2957
|
+
if (a.length !== b.length) return false;
|
|
2958
|
+
let mismatch = 0;
|
|
2959
|
+
for (let i = 0; i < a.length; i++) {
|
|
2960
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2961
|
+
}
|
|
2962
|
+
return mismatch === 0;
|
|
2963
|
+
}
|
|
2964
|
+
function buildChildEnv(platform, envAllow) {
|
|
2965
|
+
const env = {};
|
|
2966
|
+
for (const key of ALLOWED_ENV_KEYS) {
|
|
2967
|
+
if (platform.env[key]) env[key] = platform.env[key];
|
|
2968
|
+
}
|
|
2969
|
+
for (const entry of envAllow) {
|
|
2970
|
+
const eqIdx = entry.indexOf("=");
|
|
2971
|
+
if (eqIdx > 0) {
|
|
2972
|
+
env[entry.slice(0, eqIdx)] = entry.slice(eqIdx + 1);
|
|
2973
|
+
} else if (platform.env[entry]) {
|
|
2974
|
+
env[entry] = platform.env[entry];
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return env;
|
|
2978
|
+
}
|
|
2979
|
+
function capOutput(value, limit) {
|
|
2980
|
+
if (value.length <= limit) return value;
|
|
2981
|
+
const half = Math.floor(limit / 2);
|
|
2982
|
+
const cut = value.length - limit;
|
|
2983
|
+
return `${value.slice(0, half)}
|
|
2984
|
+
\u2026 ${cut} characters cut \u2026
|
|
2985
|
+
${value.slice(value.length - half)}`;
|
|
2986
|
+
}
|
|
2987
|
+
var inflightProcs = /* @__PURE__ */ new Set();
|
|
2988
|
+
async function runCommand(platform, cmdPath, argv, childEnv, stdin, timeoutMs, maxOutput, cwd) {
|
|
2989
|
+
let proc;
|
|
2990
|
+
try {
|
|
2991
|
+
proc = await platform.spawn({
|
|
2992
|
+
argv: [cmdPath, ...argv],
|
|
2993
|
+
env: childEnv,
|
|
2994
|
+
cwd
|
|
2995
|
+
});
|
|
2996
|
+
} catch (err) {
|
|
2997
|
+
return { stdout: "", stderr: `spawn failed: ${err.message}`, exitCode: 127 };
|
|
2998
|
+
}
|
|
2999
|
+
inflightProcs.add(proc);
|
|
3000
|
+
if (stdin !== void 0) {
|
|
3001
|
+
const writer = proc.stdin.getWriter();
|
|
3002
|
+
await writer.write(new TextEncoder().encode(stdin));
|
|
3003
|
+
await writer.close();
|
|
3004
|
+
} else {
|
|
3005
|
+
try {
|
|
3006
|
+
await proc.stdin.getWriter().close();
|
|
3007
|
+
} catch {
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
const stdoutChunks = [];
|
|
3011
|
+
const stderrChunks = [];
|
|
3012
|
+
const readStream = async (stream, into) => {
|
|
3013
|
+
const r = stream.getReader();
|
|
3014
|
+
try {
|
|
3015
|
+
for (; ; ) {
|
|
3016
|
+
const { done, value } = await r.read();
|
|
3017
|
+
if (done) break;
|
|
3018
|
+
into.push(value);
|
|
3019
|
+
}
|
|
3020
|
+
} finally {
|
|
3021
|
+
r.releaseLock();
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
const timer = new Promise(
|
|
3025
|
+
(resolve) => setTimeout(() => resolve("timeout"), timeoutMs)
|
|
3026
|
+
);
|
|
3027
|
+
const work = Promise.all([
|
|
3028
|
+
readStream(proc.stdout, stdoutChunks),
|
|
3029
|
+
readStream(proc.stderr, stderrChunks),
|
|
3030
|
+
proc.wait()
|
|
3031
|
+
]);
|
|
3032
|
+
const result = await Promise.race([work, timer]);
|
|
3033
|
+
if (result === "timeout") {
|
|
3034
|
+
proc.kill("SIGTERM");
|
|
3035
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
3036
|
+
try {
|
|
3037
|
+
proc.kill("SIGKILL");
|
|
3038
|
+
} catch {
|
|
3039
|
+
}
|
|
3040
|
+
inflightProcs.delete(proc);
|
|
3041
|
+
return { stdout: "", stderr: "timed out", exitCode: 124 };
|
|
3042
|
+
}
|
|
3043
|
+
inflightProcs.delete(proc);
|
|
3044
|
+
const [, , exitResult] = result;
|
|
3045
|
+
const dec = new TextDecoder();
|
|
3046
|
+
return {
|
|
3047
|
+
stdout: capOutput(dec.decode(concat(stdoutChunks)), maxOutput),
|
|
3048
|
+
stderr: capOutput(dec.decode(concat(stderrChunks)), maxOutput),
|
|
3049
|
+
exitCode: exitResult.status
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
function concat(chunks) {
|
|
3053
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
3054
|
+
const out = new Uint8Array(total);
|
|
3055
|
+
let offset = 0;
|
|
3056
|
+
for (const c of chunks) {
|
|
3057
|
+
out.set(c, offset);
|
|
3058
|
+
offset += c.length;
|
|
3059
|
+
}
|
|
3060
|
+
return out;
|
|
3061
|
+
}
|
|
3062
|
+
async function killInflight() {
|
|
3063
|
+
for (const proc of inflightProcs) {
|
|
3064
|
+
try {
|
|
3065
|
+
proc.kill("SIGTERM");
|
|
3066
|
+
} catch {
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (inflightProcs.size > 0) {
|
|
3070
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
3071
|
+
for (const proc of inflightProcs) {
|
|
3072
|
+
try {
|
|
3073
|
+
proc.kill("SIGKILL");
|
|
3074
|
+
} catch {
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
inflightProcs.clear();
|
|
3079
|
+
}
|
|
3080
|
+
async function runHelpForCli(platform, filePath, childEnv, cwd) {
|
|
3081
|
+
const dashed = await runCommand(
|
|
3082
|
+
platform,
|
|
3083
|
+
filePath,
|
|
3084
|
+
["--help"],
|
|
3085
|
+
childEnv,
|
|
3086
|
+
void 0,
|
|
3087
|
+
1e4,
|
|
3088
|
+
MAX_OUTPUT_DEFAULT,
|
|
3089
|
+
cwd
|
|
3090
|
+
);
|
|
3091
|
+
if (dashed.exitCode === 0 && dashed.stdout.trim()) return dashed.stdout;
|
|
3092
|
+
const plain = await runCommand(
|
|
3093
|
+
platform,
|
|
3094
|
+
filePath,
|
|
3095
|
+
["help"],
|
|
3096
|
+
childEnv,
|
|
3097
|
+
void 0,
|
|
3098
|
+
1e4,
|
|
3099
|
+
MAX_OUTPUT_DEFAULT,
|
|
3100
|
+
cwd
|
|
3101
|
+
);
|
|
3102
|
+
if (plain.exitCode === 0 && plain.stdout.trim()) return plain.stdout;
|
|
3103
|
+
return dashed.stdout.trim() ? dashed.stdout : "";
|
|
3104
|
+
}
|
|
3105
|
+
async function expandGroupCommands(platform, filePath, commands, childEnv, cwd) {
|
|
3106
|
+
const topNames = new Set(commands.map((c) => c.path[0] ?? ""));
|
|
3107
|
+
let current = commands;
|
|
3108
|
+
for (const command of commands) {
|
|
3109
|
+
if (!isGroupCandidate(command)) continue;
|
|
3110
|
+
const group = command.path[0] ?? "";
|
|
3111
|
+
const nested = await runCommand(
|
|
3112
|
+
platform,
|
|
3113
|
+
filePath,
|
|
3114
|
+
[group, "--help"],
|
|
3115
|
+
childEnv,
|
|
3116
|
+
void 0,
|
|
3117
|
+
1e4,
|
|
3118
|
+
MAX_OUTPUT_DEFAULT,
|
|
3119
|
+
cwd
|
|
3120
|
+
);
|
|
3121
|
+
if (nested.exitCode !== 0 || !nested.stdout.trim()) continue;
|
|
3122
|
+
const replaced = nestGroup(current, group, parseHelp(nested.stdout), topNames);
|
|
3123
|
+
if (replaced) current = replaced;
|
|
3124
|
+
}
|
|
3125
|
+
return current;
|
|
3126
|
+
}
|
|
3127
|
+
async function discoverCli(platform, filePath, explicitName, childEnv, cwd) {
|
|
3128
|
+
const mcp = await runCommand(
|
|
3129
|
+
platform,
|
|
3130
|
+
filePath,
|
|
3131
|
+
["--mcp"],
|
|
3132
|
+
childEnv,
|
|
3133
|
+
void 0,
|
|
3134
|
+
1e4,
|
|
3135
|
+
MAX_OUTPUT_DEFAULT,
|
|
3136
|
+
cwd
|
|
3137
|
+
);
|
|
3138
|
+
const override = mcp.exitCode === 0 ? parseMcpOverride(mcp.stdout) : null;
|
|
3139
|
+
const helpText2 = override ? "" : await runHelpForCli(platform, filePath, childEnv, cwd);
|
|
3140
|
+
if (!override && !helpText2.trim()) {
|
|
3141
|
+
throw new Error(`${filePath} did not print help or --mcp descriptor`);
|
|
3142
|
+
}
|
|
3143
|
+
const parsed = override ?? parseHelp(helpText2);
|
|
3144
|
+
let commands = parsed.commands;
|
|
3145
|
+
if (!override) {
|
|
3146
|
+
commands = attachGlobalFlags(commands, parsed.globalFlags);
|
|
3147
|
+
commands = await expandGroupCommands(platform, filePath, commands, childEnv, cwd);
|
|
3148
|
+
}
|
|
3149
|
+
return { name: cliPrefix(helpText2, filePath, explicitName), path: filePath, commands };
|
|
3150
|
+
}
|
|
3151
|
+
function createToolExecutor(platform, clis, allTools, childEnv, options, serveCwd) {
|
|
3152
|
+
const rpcTools = allTools.map((t) => ({
|
|
3153
|
+
name: t.name,
|
|
3154
|
+
description: t.description,
|
|
3155
|
+
inputSchema: t.inputSchema
|
|
3156
|
+
}));
|
|
3157
|
+
const queues = /* @__PURE__ */ new Map();
|
|
3158
|
+
function enqueue(key, job) {
|
|
3159
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
3160
|
+
const run2 = previous.then(job, job);
|
|
3161
|
+
queues.set(
|
|
3162
|
+
key,
|
|
3163
|
+
run2.then(
|
|
3164
|
+
() => void 0,
|
|
3165
|
+
() => void 0
|
|
3166
|
+
)
|
|
3167
|
+
);
|
|
3168
|
+
return run2;
|
|
3169
|
+
}
|
|
3170
|
+
function invokeTemplateTool(tool, args) {
|
|
3171
|
+
const templateArgs = {};
|
|
3172
|
+
if (args && typeof args === "object") {
|
|
3173
|
+
for (const [k, v] of Object.entries(args)) {
|
|
3174
|
+
templateArgs[k] = String(v);
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
const argv = templateToArgv(tool.description, templateArgs);
|
|
3178
|
+
return enqueue(
|
|
3179
|
+
"__template__",
|
|
3180
|
+
() => runCommand(
|
|
3181
|
+
platform,
|
|
3182
|
+
argv[0],
|
|
3183
|
+
argv.slice(1),
|
|
3184
|
+
childEnv,
|
|
3185
|
+
void 0,
|
|
3186
|
+
options.callTimeout,
|
|
3187
|
+
options.maxOutput,
|
|
3188
|
+
serveCwd
|
|
3189
|
+
)
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
async function executeTool(tool, cli, args) {
|
|
3193
|
+
if (tool.kind === "help") {
|
|
3194
|
+
const result = await runCommand(
|
|
3195
|
+
platform,
|
|
3196
|
+
cli.path,
|
|
3197
|
+
["--help"],
|
|
3198
|
+
childEnv,
|
|
3199
|
+
void 0,
|
|
3200
|
+
options.callTimeout,
|
|
3201
|
+
options.maxOutput,
|
|
3202
|
+
serveCwd
|
|
3203
|
+
);
|
|
3204
|
+
if (result.exitCode === 0) return result;
|
|
3205
|
+
return runCommand(
|
|
3206
|
+
platform,
|
|
3207
|
+
cli.path,
|
|
3208
|
+
["help"],
|
|
3209
|
+
childEnv,
|
|
3210
|
+
void 0,
|
|
3211
|
+
options.callTimeout,
|
|
3212
|
+
options.maxOutput,
|
|
3213
|
+
serveCwd
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
if (tool.kind === "invoke") {
|
|
3217
|
+
const parsed2 = readToolArguments(args ?? {});
|
|
3218
|
+
if (parsed2.error) return { stdout: "", stderr: parsed2.error, exitCode: 2 };
|
|
3219
|
+
if (!parsed2.argv) return { stdout: "", stderr: "argv is required", exitCode: 2 };
|
|
3220
|
+
return runCommand(
|
|
3221
|
+
platform,
|
|
3222
|
+
cli.path,
|
|
3223
|
+
parsed2.argv,
|
|
3224
|
+
childEnv,
|
|
3225
|
+
parsed2.stdin,
|
|
3226
|
+
options.callTimeout,
|
|
3227
|
+
options.maxOutput,
|
|
3228
|
+
serveCwd
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
const parsed = readToolArguments(args ?? {});
|
|
3232
|
+
const command = cli.commands.find((c) => c.path.join("_") === tool.commandPath.join("_"));
|
|
3233
|
+
if (!command) return { stdout: "", stderr: `unknown command ${tool.name}`, exitCode: 2 };
|
|
3234
|
+
const planned = argvForCommand(command, parsed);
|
|
3235
|
+
if ("error" in planned) return { stdout: "", stderr: planned.error, exitCode: 2 };
|
|
3236
|
+
return runCommand(
|
|
3237
|
+
platform,
|
|
3238
|
+
cli.path,
|
|
3239
|
+
planned.argv,
|
|
3240
|
+
childEnv,
|
|
3241
|
+
planned.stdin,
|
|
3242
|
+
options.callTimeout,
|
|
3243
|
+
options.maxOutput,
|
|
3244
|
+
serveCwd
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
async function invoke(name, args) {
|
|
3248
|
+
const tool = allTools.find((t) => t.name === name);
|
|
3249
|
+
if (!tool) return { stdout: "", stderr: `unknown tool ${name}`, exitCode: 1 };
|
|
3250
|
+
if (tool.cli === "" && tool.commandPath.length === 0) {
|
|
3251
|
+
return invokeTemplateTool(tool, args);
|
|
3252
|
+
}
|
|
3253
|
+
const cli = clis.find((c) => c.name === tool.cli);
|
|
3254
|
+
if (!cli) return { stdout: "", stderr: `unknown cli ${tool.cli}`, exitCode: 1 };
|
|
3255
|
+
return enqueue(cli.name, () => executeTool(tool, cli, args));
|
|
3256
|
+
}
|
|
3257
|
+
return { invoke, rpcTools };
|
|
3258
|
+
}
|
|
3259
|
+
async function runServe(platform, options, version) {
|
|
3260
|
+
let resolvedBearer;
|
|
3261
|
+
if (options.bearer) {
|
|
3262
|
+
const expanded = expandBearerToken(options.bearer, platform.env);
|
|
3263
|
+
if (!expanded) {
|
|
3264
|
+
await platform.write(
|
|
3265
|
+
2,
|
|
3266
|
+
"mcpcat serve: --bearer requires a ${NAME} variable reference, not a literal token. Use --bearer '${MY_TOKEN}' and set MY_TOKEN in the environment.\n"
|
|
3267
|
+
);
|
|
3268
|
+
return 2;
|
|
3269
|
+
}
|
|
3270
|
+
resolvedBearer = expanded;
|
|
3271
|
+
}
|
|
3272
|
+
const serveCwd = options.cwd ?? platform.cwd;
|
|
3273
|
+
const childEnv = buildChildEnv(platform, options.envAllow);
|
|
3274
|
+
const clis = [];
|
|
3275
|
+
for (const cmd of options.commands) {
|
|
3276
|
+
try {
|
|
3277
|
+
const cli = await discoverCli(platform, cmd.path, cmd.name, childEnv, serveCwd);
|
|
3278
|
+
clis.push(cli);
|
|
3279
|
+
} catch (err) {
|
|
3280
|
+
await platform.write(2, `mcpcat serve: ${err.message}
|
|
3281
|
+
`);
|
|
3282
|
+
return 2;
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
const allTools = clis.flatMap((cli) => buildTools(cli.name, cli.commands));
|
|
3286
|
+
for (const spec of options.toolTemplates) {
|
|
3287
|
+
const tool = parseToolTemplate(spec);
|
|
3288
|
+
if (!tool) {
|
|
3289
|
+
await platform.write(2, `mcpcat serve: invalid --tool spec: ${spec}
|
|
3290
|
+
`);
|
|
3291
|
+
return 2;
|
|
3292
|
+
}
|
|
3293
|
+
allTools.push(tool);
|
|
3294
|
+
}
|
|
3295
|
+
const executor = createToolExecutor(platform, clis, allTools, childEnv, options, serveCwd);
|
|
3296
|
+
const serverName = "mcpcat-serve";
|
|
3297
|
+
if (options.transport === "stdio") {
|
|
3298
|
+
return runStdioServer(platform, executor.rpcTools, executor.invoke, serverName, version);
|
|
3299
|
+
}
|
|
3300
|
+
return runHttpServer(
|
|
3301
|
+
platform,
|
|
3302
|
+
options,
|
|
3303
|
+
executor.rpcTools,
|
|
3304
|
+
executor.invoke,
|
|
3305
|
+
serverName,
|
|
3306
|
+
version,
|
|
3307
|
+
resolvedBearer
|
|
3308
|
+
);
|
|
3309
|
+
}
|
|
3310
|
+
async function runStdioServer(platform, tools, call, serverName, version) {
|
|
3311
|
+
const reader = platform.stdin.getReader();
|
|
3312
|
+
let buffer = "";
|
|
3313
|
+
const dec = new TextDecoder();
|
|
3314
|
+
const enc = new TextEncoder();
|
|
3315
|
+
try {
|
|
3316
|
+
for (; ; ) {
|
|
3317
|
+
const { done, value } = await reader.read();
|
|
3318
|
+
if (done) break;
|
|
3319
|
+
buffer += dec.decode(value, { stream: true });
|
|
3320
|
+
let nlIdx;
|
|
3321
|
+
while ((nlIdx = buffer.indexOf("\n")) >= 0) {
|
|
3322
|
+
const line = buffer.slice(0, nlIdx).trim();
|
|
3323
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
3324
|
+
if (!line) continue;
|
|
3325
|
+
const outcome = await handleJsonRpc(line, tools, call, serverName, version);
|
|
3326
|
+
if (outcome.status === 202) continue;
|
|
3327
|
+
const writer = platform.stdout.getWriter();
|
|
3328
|
+
await writer.write(enc.encode(`${outcome.body}
|
|
3329
|
+
`));
|
|
3330
|
+
writer.releaseLock();
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
} finally {
|
|
3334
|
+
reader.releaseLock();
|
|
3335
|
+
}
|
|
3336
|
+
return 0;
|
|
3337
|
+
}
|
|
3338
|
+
async function runHttpServer(platform, options, tools, call, serverName, version, resolvedBearer) {
|
|
3339
|
+
if (!platform.listen) {
|
|
3340
|
+
await platform.write(2, "mcpcat serve: HTTP transport requires platform.listen\n");
|
|
3341
|
+
return 3;
|
|
3342
|
+
}
|
|
3343
|
+
const boundLoopback = options.host === "127.0.0.1" || options.host === "localhost" || options.host === "::1";
|
|
3344
|
+
if (!boundLoopback && !resolvedBearer) {
|
|
3345
|
+
await platform.write(2, "mcpcat serve: --bearer is required when binding off loopback\n");
|
|
3346
|
+
return 2;
|
|
3347
|
+
}
|
|
3348
|
+
const allowedOrigins = new Set(options.allowOrigins);
|
|
3349
|
+
const listener = await platform.listen({ host: options.host, port: options.port });
|
|
3350
|
+
const actualPort = listener.port;
|
|
3351
|
+
const listenUrl = `http://${options.host}:${actualPort}/mcp`;
|
|
3352
|
+
if (options.json) {
|
|
3353
|
+
await platform.write(1, `${JSON.stringify({ url: listenUrl })}
|
|
3354
|
+
`);
|
|
3355
|
+
}
|
|
3356
|
+
await platform.write(2, `mcpcat serve: listening on ${listenUrl}
|
|
3357
|
+
`);
|
|
3358
|
+
let shutdownRequested = false;
|
|
3359
|
+
const shutdown = () => {
|
|
3360
|
+
if (shutdownRequested) return;
|
|
3361
|
+
shutdownRequested = true;
|
|
3362
|
+
listener.close();
|
|
3363
|
+
void killInflight();
|
|
3364
|
+
};
|
|
3365
|
+
if (platform.onSignal) {
|
|
3366
|
+
platform.onSignal("SIGINT", shutdown);
|
|
3367
|
+
platform.onSignal("SIGTERM", shutdown);
|
|
3368
|
+
}
|
|
3369
|
+
const handler = (request) => handleMcpRequest(
|
|
3370
|
+
request,
|
|
3371
|
+
tools,
|
|
3372
|
+
call,
|
|
3373
|
+
serverName,
|
|
3374
|
+
version,
|
|
3375
|
+
allowedOrigins,
|
|
3376
|
+
boundLoopback,
|
|
3377
|
+
resolvedBearer
|
|
3378
|
+
);
|
|
3379
|
+
try {
|
|
3380
|
+
for await (const connection of listener) {
|
|
3381
|
+
if (shutdownRequested) break;
|
|
3382
|
+
const conn = connection;
|
|
3383
|
+
handleConnection(conn.readable, conn.writable, handler).catch(() => {
|
|
3384
|
+
}).finally(() => {
|
|
3385
|
+
try {
|
|
3386
|
+
conn.close();
|
|
3387
|
+
} catch {
|
|
3388
|
+
}
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
} catch {
|
|
3392
|
+
if (!shutdownRequested) throw new Error("listener closed unexpectedly");
|
|
3393
|
+
}
|
|
3394
|
+
return 0;
|
|
3395
|
+
}
|
|
3396
|
+
async function handleMcpRequest(request, tools, call, serverName, version, allowedOrigins, boundLoopback, resolvedBearer) {
|
|
3397
|
+
if (resolvedBearer) {
|
|
3398
|
+
const authHeader = request.headers.authorization;
|
|
3399
|
+
const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
3400
|
+
if (!provided || !constantTimeEqual(provided, resolvedBearer)) {
|
|
3401
|
+
return {
|
|
3402
|
+
status: 401,
|
|
3403
|
+
reason: "Unauthorized",
|
|
3404
|
+
headers: { "content-type": "text/plain" },
|
|
3405
|
+
body: "Unauthorized"
|
|
3406
|
+
};
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
const originCheck = checkOrigin(request.headers.origin, allowedOrigins, boundLoopback);
|
|
3410
|
+
if (!originCheck.ok) {
|
|
3411
|
+
return {
|
|
3412
|
+
status: 403,
|
|
3413
|
+
reason: "Forbidden",
|
|
3414
|
+
headers: { "content-type": "text/plain" },
|
|
3415
|
+
body: originCheck.reason ?? "Forbidden"
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
const hostCheck = checkHost(request.headers.host, boundLoopback);
|
|
3419
|
+
if (!hostCheck.ok) {
|
|
3420
|
+
return {
|
|
3421
|
+
status: 403,
|
|
3422
|
+
reason: "Forbidden",
|
|
3423
|
+
headers: { "content-type": "text/plain" },
|
|
3424
|
+
body: hostCheck.reason ?? "Forbidden"
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
if (request.target !== "/mcp") {
|
|
3428
|
+
return {
|
|
3429
|
+
status: 404,
|
|
3430
|
+
reason: "Not Found",
|
|
3431
|
+
headers: { "content-type": "text/plain" },
|
|
3432
|
+
body: "Not Found"
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
if (request.method !== "POST") {
|
|
3436
|
+
return {
|
|
3437
|
+
status: 405,
|
|
3438
|
+
reason: "Method Not Allowed",
|
|
3439
|
+
headers: { "content-type": "text/plain", allow: "POST" },
|
|
3440
|
+
body: "Method Not Allowed"
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
const outcome = await handleJsonRpc(request.body, tools, call, serverName, version);
|
|
3444
|
+
return {
|
|
3445
|
+
status: outcome.status,
|
|
3446
|
+
reason: outcome.status === 200 ? "OK" : "Accepted",
|
|
3447
|
+
headers: { "content-type": outcome.contentType },
|
|
3448
|
+
body: outcome.body
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3451
|
+
|
|
1861
3452
|
// src/config/lock.ts
|
|
1862
3453
|
var LOCK_STALE_MS = 1e4;
|
|
1863
3454
|
var LOCK_POLL_MS = 50;
|
|
@@ -2275,12 +3866,12 @@ function importFile(text, filename, names) {
|
|
|
2275
3866
|
|
|
2276
3867
|
// src/config/warnings.ts
|
|
2277
3868
|
var SECRET_HEADER = /authorization|cookie|token|key|secret|password|session/i;
|
|
2278
|
-
var
|
|
3869
|
+
var PLACEHOLDER2 = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2279
3870
|
var SCHEME = /^\s*(?:bearer|basic|token|apikey|api-key)\s+/i;
|
|
2280
3871
|
var SECRET_PARAM = /token|key|secret|password|signature|sig|auth|code/i;
|
|
2281
3872
|
function literalSecret(header, value) {
|
|
2282
3873
|
if (!SECRET_HEADER.test(header)) return false;
|
|
2283
|
-
return value.replace(
|
|
3874
|
+
return value.replace(PLACEHOLDER2, "").replace(SCHEME, "").trim() !== "";
|
|
2284
3875
|
}
|
|
2285
3876
|
function urlHasSecret(url) {
|
|
2286
3877
|
if (!URL.canParse(url)) return void 0;
|
|
@@ -2815,12 +4406,11 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
2815
4406
|
"task",
|
|
2816
4407
|
"login",
|
|
2817
4408
|
"logout",
|
|
2818
|
-
"serve",
|
|
2819
4409
|
"code",
|
|
2820
4410
|
"discover",
|
|
2821
4411
|
"inbox"
|
|
2822
4412
|
]);
|
|
2823
|
-
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe"]);
|
|
4413
|
+
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe", "serve"]);
|
|
2824
4414
|
function helpText(version) {
|
|
2825
4415
|
return `mcpcat ${version} \u2013 a curl for the Model Context Protocol
|
|
2826
4416
|
|
|
@@ -2844,7 +4434,7 @@ Configuration verbs:
|
|
|
2844
4434
|
logout <target> Remove tokens (not implemented yet)
|
|
2845
4435
|
|
|
2846
4436
|
Server and code verbs:
|
|
2847
|
-
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
4437
|
+
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
2848
4438
|
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
2849
4439
|
discover <url> Find MCP servers (not implemented yet)
|
|
2850
4440
|
inbox Discovery inbox (not implemented yet)
|
|
@@ -2978,6 +4568,21 @@ function renderCallResult(result) {
|
|
|
2978
4568
|
}
|
|
2979
4569
|
return parts.join("\n");
|
|
2980
4570
|
}
|
|
4571
|
+
var NETWORK_HINT = "connection failed (use slicc-node, slicc-swift, or the browser extension for servers without CORS)";
|
|
4572
|
+
function transportHint(err) {
|
|
4573
|
+
if (err instanceof TypeError) {
|
|
4574
|
+
const cause = err.cause;
|
|
4575
|
+
if (cause?.code === "ECONNREFUSED" || cause?.code === "ENETUNREACH") {
|
|
4576
|
+
return `connection failed: ${err.message}`;
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
if (err instanceof Error && "status" in err && err.status === 502) {
|
|
4580
|
+
const msg = err.message;
|
|
4581
|
+
if (msg) return `${NETWORK_HINT}: ${msg}`;
|
|
4582
|
+
return NETWORK_HINT;
|
|
4583
|
+
}
|
|
4584
|
+
return void 0;
|
|
4585
|
+
}
|
|
2981
4586
|
async function readDataArg(platform, dataArg) {
|
|
2982
4587
|
let raw;
|
|
2983
4588
|
if (dataArg.startsWith("@-")) {
|
|
@@ -3174,6 +4779,15 @@ async function run(platform, version, args) {
|
|
|
3174
4779
|
`);
|
|
3175
4780
|
return 1;
|
|
3176
4781
|
}
|
|
4782
|
+
if (parsed.verb === "serve") {
|
|
4783
|
+
const serveArgs = args.slice(args.indexOf("serve") + 1);
|
|
4784
|
+
const serveOptions = parseServeFlags(serveArgs);
|
|
4785
|
+
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
4786
|
+
await platform.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
4787
|
+
return 2;
|
|
4788
|
+
}
|
|
4789
|
+
return runServe(platform, serveOptions, version);
|
|
4790
|
+
}
|
|
3177
4791
|
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
3178
4792
|
await platform.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
3179
4793
|
`);
|
|
@@ -3210,7 +4824,8 @@ async function run(platform, version, args) {
|
|
|
3210
4824
|
version
|
|
3211
4825
|
});
|
|
3212
4826
|
} catch (err) {
|
|
3213
|
-
|
|
4827
|
+
const msg = transportHint(err) ?? `connection failed: ${err.message}`;
|
|
4828
|
+
await platform.write(2, `mcpcat: ${msg}
|
|
3214
4829
|
`);
|
|
3215
4830
|
return 3;
|
|
3216
4831
|
}
|
|
@@ -3263,19 +4878,64 @@ async function kernelSpawn(ctx, options) {
|
|
|
3263
4878
|
stdout: "pipe",
|
|
3264
4879
|
stderr: "pipe"
|
|
3265
4880
|
});
|
|
4881
|
+
const childPid = result.pid;
|
|
3266
4882
|
return {
|
|
3267
4883
|
stdin: result.stdin,
|
|
3268
4884
|
stdout: result.stdout,
|
|
3269
4885
|
stderr: result.stderr,
|
|
3270
4886
|
wait: () => result.wait(),
|
|
3271
|
-
kill: (signal) =>
|
|
4887
|
+
kill: (signal) => {
|
|
4888
|
+
try {
|
|
4889
|
+
ctx.kill(-childPid, signal);
|
|
4890
|
+
} catch {
|
|
4891
|
+
}
|
|
4892
|
+
try {
|
|
4893
|
+
ctx.kill(childPid, signal);
|
|
4894
|
+
} catch {
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
};
|
|
4898
|
+
}
|
|
4899
|
+
async function kernelListen(ctx, options) {
|
|
4900
|
+
const raw = await ctx.net.listen({ port: options.port, host: options.host });
|
|
4901
|
+
let closed = false;
|
|
4902
|
+
async function accept() {
|
|
4903
|
+
const conn = await raw.accept();
|
|
4904
|
+
return {
|
|
4905
|
+
readable: conn.readable,
|
|
4906
|
+
writable: conn.writable,
|
|
4907
|
+
remoteAddress: conn.remote.host,
|
|
4908
|
+
close() {
|
|
4909
|
+
conn.close();
|
|
4910
|
+
}
|
|
4911
|
+
};
|
|
4912
|
+
}
|
|
4913
|
+
return {
|
|
4914
|
+
get port() {
|
|
4915
|
+
return raw.port;
|
|
4916
|
+
},
|
|
4917
|
+
accept,
|
|
4918
|
+
close() {
|
|
4919
|
+
closed = true;
|
|
4920
|
+
raw.close();
|
|
4921
|
+
},
|
|
4922
|
+
async *[Symbol.asyncIterator]() {
|
|
4923
|
+
while (!closed) {
|
|
4924
|
+
try {
|
|
4925
|
+
yield await accept();
|
|
4926
|
+
} catch {
|
|
4927
|
+
break;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
3272
4931
|
};
|
|
3273
4932
|
}
|
|
3274
4933
|
function createKernelPlatform(ctx) {
|
|
3275
|
-
const
|
|
4934
|
+
const encoder2 = new TextEncoder();
|
|
3276
4935
|
return {
|
|
3277
4936
|
fetch: ctx.fetch,
|
|
3278
4937
|
spawn: (options) => kernelSpawn(ctx, options),
|
|
4938
|
+
listen: (options) => kernelListen(ctx, options),
|
|
3279
4939
|
fs: kernelFs(ctx),
|
|
3280
4940
|
env: ctx.env,
|
|
3281
4941
|
cwd: ctx.cwd,
|
|
@@ -3284,10 +4944,13 @@ function createKernelPlatform(ctx) {
|
|
|
3284
4944
|
stdout: ctx.stdout,
|
|
3285
4945
|
stderr: ctx.stderr,
|
|
3286
4946
|
async write(fd, data) {
|
|
3287
|
-
await ctx.write(fd, typeof data === "string" ?
|
|
4947
|
+
await ctx.write(fd, typeof data === "string" ? encoder2.encode(data) : data);
|
|
3288
4948
|
},
|
|
3289
4949
|
exit(code) {
|
|
3290
4950
|
ctx.exit(code);
|
|
4951
|
+
},
|
|
4952
|
+
onSignal(signal, handler) {
|
|
4953
|
+
ctx.signals.on(signal, handler);
|
|
3291
4954
|
}
|
|
3292
4955
|
};
|
|
3293
4956
|
}
|
|
@@ -3296,7 +4959,7 @@ function createKernelPlatform(ctx) {
|
|
|
3296
4959
|
async function main(ctx) {
|
|
3297
4960
|
const platform = createKernelPlatform(ctx);
|
|
3298
4961
|
const args = ctx.argv.slice(1);
|
|
3299
|
-
return run(platform, "0.
|
|
4962
|
+
return run(platform, "0.5.0", args);
|
|
3300
4963
|
}
|
|
3301
4964
|
export {
|
|
3302
4965
|
main as default
|