@ai-ecoverse/mcpcat 0.4.0 → 0.6.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 +1277 -254
- package/dist/kernel/mcpcat.mjs +1269 -248
- package/package.json +6 -5
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,
|
|
@@ -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);
|
|
@@ -796,7 +798,7 @@ async function consumeSseStream(stream, options) {
|
|
|
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
804
|
buffered += decoder2.decode();
|
|
@@ -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
|
}
|
|
@@ -1282,10 +1475,10 @@ async function createStatelessConnection(options) {
|
|
|
1282
1475
|
timeoutMs = 6e4,
|
|
1283
1476
|
clientInfo = { name: "mcpcat", version: "0.0.0" }
|
|
1284
1477
|
} = options;
|
|
1285
|
-
let
|
|
1478
|
+
let nextId2 = 1;
|
|
1286
1479
|
let negotiatedVersion;
|
|
1287
1480
|
async function request(method, params) {
|
|
1288
|
-
const id =
|
|
1481
|
+
const id = nextId2++;
|
|
1289
1482
|
const headers = {
|
|
1290
1483
|
"Content-Type": "application/json",
|
|
1291
1484
|
Accept: "application/json, text/event-stream",
|
|
@@ -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;
|
|
@@ -1798,65 +1996,981 @@ async function loadConfig(input) {
|
|
|
1798
1996
|
loadProjectServers(parsed, input.projectPath, servers, problems);
|
|
1799
1997
|
}
|
|
1800
1998
|
}
|
|
1801
|
-
return {
|
|
1802
|
-
servers: [...servers.values()],
|
|
1803
|
-
problems,
|
|
1804
|
-
...autoEnableCodemode !== void 0 ? { autoEnableCodemode } : {}
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
// src/config/paths.ts
|
|
1809
|
-
function userConfigDir(platform) {
|
|
1810
|
-
const piDir = platform.env.PI_CODING_AGENT_DIR;
|
|
1811
|
-
if (piDir) return piDir;
|
|
1812
|
-
const home = platform.env.HOME ?? platform.env.USERPROFILE ?? "/";
|
|
1813
|
-
return `${home}/.pi/agent`;
|
|
1814
|
-
}
|
|
1815
|
-
function userConfigPath(platform) {
|
|
1816
|
-
return `${userConfigDir(platform)}/mcp.json`;
|
|
1817
|
-
}
|
|
1818
|
-
function projectConfigPath(platform) {
|
|
1819
|
-
return `${platform.cwd}/.pi/mcp.json`;
|
|
1999
|
+
return {
|
|
2000
|
+
servers: [...servers.values()],
|
|
2001
|
+
problems,
|
|
2002
|
+
...autoEnableCodemode !== void 0 ? { autoEnableCodemode } : {}
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
// src/config/paths.ts
|
|
2007
|
+
function userConfigDir(platform) {
|
|
2008
|
+
const piDir = platform.env.PI_CODING_AGENT_DIR;
|
|
2009
|
+
if (piDir) return piDir;
|
|
2010
|
+
const home = platform.env.HOME ?? platform.env.USERPROFILE ?? "/";
|
|
2011
|
+
return `${home}/.pi/agent`;
|
|
2012
|
+
}
|
|
2013
|
+
function userConfigPath(platform) {
|
|
2014
|
+
return `${userConfigDir(platform)}/mcp.json`;
|
|
2015
|
+
}
|
|
2016
|
+
function projectConfigPath(platform) {
|
|
2017
|
+
return `${platform.cwd}/.pi/mcp.json`;
|
|
2018
|
+
}
|
|
2019
|
+
function stateDir(platform) {
|
|
2020
|
+
if (platform.env.XDG_STATE_HOME) return `${platform.env.XDG_STATE_HOME}/mcpcat`;
|
|
2021
|
+
const home = platform.env.HOME ?? platform.env.USERPROFILE ?? "/";
|
|
2022
|
+
if (platform.env.LOCALAPPDATA) return `${platform.env.LOCALAPPDATA}\\mcpcat\\state`;
|
|
2023
|
+
if (platform.env.SLICC_KERNEL) return `${home}/.local/state/mcpcat`;
|
|
2024
|
+
return `${home}/Library/Application Support/mcpcat`;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/config/index.ts
|
|
2028
|
+
var MergedConfigSource = class _MergedConfigSource {
|
|
2029
|
+
entries;
|
|
2030
|
+
config;
|
|
2031
|
+
constructor(config) {
|
|
2032
|
+
this.config = config;
|
|
2033
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
2034
|
+
for (const entry of config.servers) {
|
|
2035
|
+
this.entries.set(entry.name, entry);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async get(name) {
|
|
2039
|
+
const normalizedName = name.replace(/[-_]/g, "").toLowerCase();
|
|
2040
|
+
for (const [key, entry] of this.entries) {
|
|
2041
|
+
if (key.replace(/[-_]/g, "").toLowerCase() === normalizedName) {
|
|
2042
|
+
return entry.config;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return void 0;
|
|
2046
|
+
}
|
|
2047
|
+
entry(name) {
|
|
2048
|
+
return this.entries.get(name);
|
|
2049
|
+
}
|
|
2050
|
+
list() {
|
|
2051
|
+
return [...this.entries.values()];
|
|
2052
|
+
}
|
|
2053
|
+
static async load(platform, options) {
|
|
2054
|
+
const userPath = options?.configFile ?? userConfigPath(platform);
|
|
2055
|
+
const projectPath = options?.configFile ? void 0 : projectConfigPath(platform);
|
|
2056
|
+
const config = await loadConfig({
|
|
2057
|
+
userPath,
|
|
2058
|
+
projectPath,
|
|
2059
|
+
trustProject: options?.trustProject ?? false,
|
|
2060
|
+
fs: platform.fs
|
|
2061
|
+
});
|
|
2062
|
+
return new _MergedConfigSource(config);
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/config/lock.ts
|
|
2067
|
+
var LOCK_STALE_MS = 1e4;
|
|
2068
|
+
var LOCK_POLL_MS = 50;
|
|
2069
|
+
var LOCK_MAX_WAIT_MS = 5e3;
|
|
2070
|
+
function isEexist(err) {
|
|
2071
|
+
if (err instanceof Error && "code" in err)
|
|
2072
|
+
return err.code === "EEXIST";
|
|
2073
|
+
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
2074
|
+
return false;
|
|
2075
|
+
}
|
|
2076
|
+
async function removeStaleLock(fs, lockPath) {
|
|
2077
|
+
try {
|
|
2078
|
+
const s = await fs.stat(lockPath);
|
|
2079
|
+
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
2080
|
+
try {
|
|
2081
|
+
await fs.rmdir(lockPath);
|
|
2082
|
+
} catch {
|
|
2083
|
+
}
|
|
2084
|
+
return true;
|
|
2085
|
+
}
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
async function acquireLock(fs, configPath) {
|
|
2091
|
+
const lockPath = `${configPath}.lock`;
|
|
2092
|
+
const start = Date.now();
|
|
2093
|
+
while (true) {
|
|
2094
|
+
try {
|
|
2095
|
+
await fs.mkdirExclusive(lockPath);
|
|
2096
|
+
break;
|
|
2097
|
+
} catch (err) {
|
|
2098
|
+
if (!isEexist(err)) throw err;
|
|
2099
|
+
await removeStaleLock(fs, lockPath);
|
|
2100
|
+
}
|
|
2101
|
+
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
2102
|
+
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
2103
|
+
}
|
|
2104
|
+
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
2105
|
+
}
|
|
2106
|
+
let released = false;
|
|
2107
|
+
return async () => {
|
|
2108
|
+
if (released) return;
|
|
2109
|
+
released = true;
|
|
2110
|
+
try {
|
|
2111
|
+
await fs.rmdir(lockPath);
|
|
2112
|
+
} catch {
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/config/edit.ts
|
|
2118
|
+
function isRecord2(value) {
|
|
2119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2120
|
+
}
|
|
2121
|
+
function detectIndent(text) {
|
|
2122
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
2123
|
+
return match ? match[1] : " ";
|
|
2124
|
+
}
|
|
2125
|
+
async function readText(fs, path) {
|
|
2126
|
+
try {
|
|
2127
|
+
const data = await fs.readFile(path);
|
|
2128
|
+
return new TextDecoder().decode(data);
|
|
2129
|
+
} catch {
|
|
2130
|
+
return void 0;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
async function editMcpServers(fs, path, edit) {
|
|
2134
|
+
const release = await acquireLock(fs, path);
|
|
2135
|
+
try {
|
|
2136
|
+
const text = await readText(fs, path);
|
|
2137
|
+
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
2138
|
+
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
2139
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
2140
|
+
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
2141
|
+
if (!edit(servers, parsed)) return { changed: false };
|
|
2142
|
+
const indent = text ? detectIndent(text) : " ";
|
|
2143
|
+
const output = `${JSON.stringify(parsed, null, indent)}
|
|
2144
|
+
`;
|
|
2145
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
2146
|
+
if (dir) {
|
|
2147
|
+
try {
|
|
2148
|
+
await fs.mkdir(dir);
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
2153
|
+
await fs.writeFile(tmpPath, output);
|
|
2154
|
+
await fs.rename(tmpPath, path);
|
|
2155
|
+
return { changed: true };
|
|
2156
|
+
} finally {
|
|
2157
|
+
await release();
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
async function addServer(fs, path, name, config) {
|
|
2161
|
+
let replaced = false;
|
|
2162
|
+
await editMcpServers(fs, path, (servers, parsed) => {
|
|
2163
|
+
const target = servers ?? {};
|
|
2164
|
+
replaced = target[name] !== void 0;
|
|
2165
|
+
target[name] = config;
|
|
2166
|
+
parsed.mcpServers = target;
|
|
2167
|
+
return true;
|
|
2168
|
+
});
|
|
2169
|
+
return { replaced };
|
|
2170
|
+
}
|
|
2171
|
+
async function removeServer(fs, path, name) {
|
|
2172
|
+
let removed = false;
|
|
2173
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2174
|
+
if (!servers || servers[name] === void 0) return false;
|
|
2175
|
+
delete servers[name];
|
|
2176
|
+
removed = true;
|
|
2177
|
+
return true;
|
|
2178
|
+
});
|
|
2179
|
+
return removed;
|
|
2180
|
+
}
|
|
2181
|
+
async function updateServer(fs, path, name, patch) {
|
|
2182
|
+
let updated = false;
|
|
2183
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2184
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2185
|
+
const entry = servers[name];
|
|
2186
|
+
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
2187
|
+
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
2188
|
+
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
2189
|
+
updated = true;
|
|
2190
|
+
return true;
|
|
2191
|
+
});
|
|
2192
|
+
return updated;
|
|
2193
|
+
}
|
|
2194
|
+
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
2195
|
+
let updated = false;
|
|
2196
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2197
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2198
|
+
const entry = servers[name];
|
|
2199
|
+
if (toolGlob) {
|
|
2200
|
+
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
2201
|
+
te[toolGlob] = exposure;
|
|
2202
|
+
entry.toolExposure = te;
|
|
2203
|
+
} else {
|
|
2204
|
+
entry.exposure = exposure;
|
|
2205
|
+
}
|
|
2206
|
+
updated = true;
|
|
2207
|
+
return true;
|
|
2208
|
+
});
|
|
2209
|
+
return updated;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
// src/discovery/extract.ts
|
|
2213
|
+
var nextId = 1;
|
|
2214
|
+
function candidateId() {
|
|
2215
|
+
return String(nextId++);
|
|
2216
|
+
}
|
|
2217
|
+
function resetIdCounter(start = 1) {
|
|
2218
|
+
nextId = start;
|
|
2219
|
+
}
|
|
2220
|
+
function isRecord3(v) {
|
|
2221
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2222
|
+
}
|
|
2223
|
+
function checkUrnPublisher(identifier, originHost) {
|
|
2224
|
+
const urnMatch = /^urn:air:([^:]+):/.exec(identifier);
|
|
2225
|
+
if (!urnMatch) return void 0;
|
|
2226
|
+
const urnDomain = urnMatch[1];
|
|
2227
|
+
if (domainMatch(urnDomain, originHost)) {
|
|
2228
|
+
return { type: "urn-publisher", detail: `${urnDomain} matches ${originHost}` };
|
|
2229
|
+
}
|
|
2230
|
+
return { type: "urn-publisher-mismatch", detail: `${urnDomain} does not match ${originHost}` };
|
|
2231
|
+
}
|
|
2232
|
+
function checkTrustManifest(entry, originHost) {
|
|
2233
|
+
if (!isRecord3(entry.trustManifest) || typeof entry.trustManifest.identity !== "string")
|
|
2234
|
+
return void 0;
|
|
2235
|
+
const identity = entry.trustManifest.identity;
|
|
2236
|
+
if (domainMatch(identity, originHost)) {
|
|
2237
|
+
return { type: "trust-manifest", detail: `identity ${identity} matches origin` };
|
|
2238
|
+
}
|
|
2239
|
+
return {
|
|
2240
|
+
type: "trust-manifest-mismatch",
|
|
2241
|
+
detail: `identity ${identity} does not match ${originHost}`
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
function extractArd(event) {
|
|
2245
|
+
const candidates = [];
|
|
2246
|
+
const summary = event.summary;
|
|
2247
|
+
if (!isRecord3(summary)) return candidates;
|
|
2248
|
+
const entries = summary.entries;
|
|
2249
|
+
if (!Array.isArray(entries)) return candidates;
|
|
2250
|
+
for (const entry of entries) {
|
|
2251
|
+
if (!isRecord3(entry)) continue;
|
|
2252
|
+
if (entry.type !== "application/mcp-server-card+json") continue;
|
|
2253
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
2254
|
+
if (!url) continue;
|
|
2255
|
+
const trust = [];
|
|
2256
|
+
const originHost = hostFromUrl(event.origin);
|
|
2257
|
+
const identifier = typeof entry.identifier === "string" ? entry.identifier : void 0;
|
|
2258
|
+
if (identifier) {
|
|
2259
|
+
const urnSignal = checkUrnPublisher(identifier, originHost);
|
|
2260
|
+
if (urnSignal) trust.push(urnSignal);
|
|
2261
|
+
}
|
|
2262
|
+
const tmSignal = checkTrustManifest(entry, originHost);
|
|
2263
|
+
if (tmSignal) trust.push(tmSignal);
|
|
2264
|
+
candidates.push({
|
|
2265
|
+
id: candidateId(),
|
|
2266
|
+
endpoint: url,
|
|
2267
|
+
standard: "ard",
|
|
2268
|
+
origin: event.origin,
|
|
2269
|
+
name: typeof entry.displayName === "string" ? entry.displayName : void 0,
|
|
2270
|
+
transport: "http",
|
|
2271
|
+
trust,
|
|
2272
|
+
status: event.status,
|
|
2273
|
+
reason: event.reason,
|
|
2274
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
return candidates;
|
|
2278
|
+
}
|
|
2279
|
+
function extractAiCatalog(event) {
|
|
2280
|
+
const candidates = [];
|
|
2281
|
+
const summary = event.summary;
|
|
2282
|
+
if (!isRecord3(summary)) return candidates;
|
|
2283
|
+
const entries = summary.entries;
|
|
2284
|
+
if (!Array.isArray(entries)) return candidates;
|
|
2285
|
+
for (const entry of entries) {
|
|
2286
|
+
if (!isRecord3(entry)) continue;
|
|
2287
|
+
if (entry.type !== "application/mcp-server-card+json" && entry.type !== "mcp-server") continue;
|
|
2288
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
2289
|
+
if (!url) continue;
|
|
2290
|
+
candidates.push({
|
|
2291
|
+
id: candidateId(),
|
|
2292
|
+
endpoint: url,
|
|
2293
|
+
standard: "ai-catalog",
|
|
2294
|
+
origin: event.origin,
|
|
2295
|
+
name: typeof entry.displayName === "string" ? entry.displayName : void 0,
|
|
2296
|
+
transport: "http",
|
|
2297
|
+
trust: [],
|
|
2298
|
+
status: event.status,
|
|
2299
|
+
reason: event.reason,
|
|
2300
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
return candidates;
|
|
2304
|
+
}
|
|
2305
|
+
function extractUcp(event) {
|
|
2306
|
+
const candidates = [];
|
|
2307
|
+
const summary = event.summary;
|
|
2308
|
+
if (!isRecord3(summary)) return candidates;
|
|
2309
|
+
const services = summary.services;
|
|
2310
|
+
if (!isRecord3(services)) return candidates;
|
|
2311
|
+
for (const [svcName, svcValue] of Object.entries(services)) {
|
|
2312
|
+
if (!Array.isArray(svcValue)) continue;
|
|
2313
|
+
for (const transport of svcValue) {
|
|
2314
|
+
if (!isRecord3(transport)) continue;
|
|
2315
|
+
if (transport.transport !== "mcp") continue;
|
|
2316
|
+
const endpoint = typeof transport.endpoint === "string" ? transport.endpoint : void 0;
|
|
2317
|
+
if (!endpoint) continue;
|
|
2318
|
+
candidates.push({
|
|
2319
|
+
id: candidateId(),
|
|
2320
|
+
endpoint,
|
|
2321
|
+
standard: "ucp",
|
|
2322
|
+
origin: event.origin,
|
|
2323
|
+
name: svcName,
|
|
2324
|
+
transport: "http",
|
|
2325
|
+
trust: [],
|
|
2326
|
+
status: event.status,
|
|
2327
|
+
reason: event.reason,
|
|
2328
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return candidates;
|
|
2333
|
+
}
|
|
2334
|
+
function extractPoppy(event) {
|
|
2335
|
+
const candidates = [];
|
|
2336
|
+
const summary = event.summary;
|
|
2337
|
+
if (!isRecord3(summary)) return candidates;
|
|
2338
|
+
const apis = summary.apis;
|
|
2339
|
+
if (!Array.isArray(apis)) return candidates;
|
|
2340
|
+
for (const api of apis) {
|
|
2341
|
+
if (!isRecord3(api)) continue;
|
|
2342
|
+
if (api.type !== "mcp") continue;
|
|
2343
|
+
const url = typeof api.url === "string" ? api.url : void 0;
|
|
2344
|
+
if (!url) continue;
|
|
2345
|
+
candidates.push({
|
|
2346
|
+
id: candidateId(),
|
|
2347
|
+
endpoint: url,
|
|
2348
|
+
standard: "poppy",
|
|
2349
|
+
origin: event.origin,
|
|
2350
|
+
transport: "http",
|
|
2351
|
+
trust: [],
|
|
2352
|
+
status: event.status,
|
|
2353
|
+
reason: event.reason,
|
|
2354
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
return candidates;
|
|
2358
|
+
}
|
|
2359
|
+
function extractNlweb(event) {
|
|
2360
|
+
const summary = event.summary;
|
|
2361
|
+
if (!isRecord3(summary)) return [];
|
|
2362
|
+
const mcpUrl = typeof summary.mcpUrl === "string" ? summary.mcpUrl : void 0;
|
|
2363
|
+
if (!mcpUrl) return [];
|
|
2364
|
+
return [
|
|
2365
|
+
{
|
|
2366
|
+
id: candidateId(),
|
|
2367
|
+
endpoint: mcpUrl,
|
|
2368
|
+
standard: "nlweb",
|
|
2369
|
+
origin: event.origin,
|
|
2370
|
+
transport: "http",
|
|
2371
|
+
trust: [],
|
|
2372
|
+
status: event.status,
|
|
2373
|
+
reason: event.reason,
|
|
2374
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2375
|
+
}
|
|
2376
|
+
];
|
|
2377
|
+
}
|
|
2378
|
+
function extractWebmcp(event) {
|
|
2379
|
+
return [
|
|
2380
|
+
{
|
|
2381
|
+
id: candidateId(),
|
|
2382
|
+
endpoint: event.url,
|
|
2383
|
+
standard: "webmcp",
|
|
2384
|
+
origin: event.origin,
|
|
2385
|
+
transport: "webmcp",
|
|
2386
|
+
trust: [],
|
|
2387
|
+
status: event.status,
|
|
2388
|
+
reason: event.reason,
|
|
2389
|
+
suggestion: "mcpcat serve --webmcp",
|
|
2390
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2391
|
+
}
|
|
2392
|
+
];
|
|
2393
|
+
}
|
|
2394
|
+
function hostFromUrl(url) {
|
|
2395
|
+
try {
|
|
2396
|
+
return new URL(url).hostname;
|
|
2397
|
+
} catch {
|
|
2398
|
+
return "";
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
var TWO_LABEL_TLDS = /* @__PURE__ */ new Set([
|
|
2402
|
+
"co.uk",
|
|
2403
|
+
"org.uk",
|
|
2404
|
+
"ac.uk",
|
|
2405
|
+
"co.jp",
|
|
2406
|
+
"or.jp",
|
|
2407
|
+
"co.kr",
|
|
2408
|
+
"co.nz",
|
|
2409
|
+
"co.za",
|
|
2410
|
+
"com.au",
|
|
2411
|
+
"com.br",
|
|
2412
|
+
"com.cn",
|
|
2413
|
+
"com.mx",
|
|
2414
|
+
"com.tw",
|
|
2415
|
+
"net.au",
|
|
2416
|
+
"org.au",
|
|
2417
|
+
"co.in",
|
|
2418
|
+
"com.sg",
|
|
2419
|
+
"co.il"
|
|
2420
|
+
]);
|
|
2421
|
+
function registrableDomain(host) {
|
|
2422
|
+
const h = host.toLowerCase().replace(/^www\./, "");
|
|
2423
|
+
const parts = h.split(".");
|
|
2424
|
+
if (parts.length <= 2) return h;
|
|
2425
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
2426
|
+
if (TWO_LABEL_TLDS.has(lastTwo) && parts.length > 2) {
|
|
2427
|
+
return parts.slice(-3).join(".");
|
|
2428
|
+
}
|
|
2429
|
+
return lastTwo;
|
|
2430
|
+
}
|
|
2431
|
+
function domainMatch(a, b) {
|
|
2432
|
+
return registrableDomain(a) === registrableDomain(b);
|
|
2433
|
+
}
|
|
2434
|
+
function extractMcp(event) {
|
|
2435
|
+
const summary = event.summary;
|
|
2436
|
+
if (!isRecord3(summary)) return [];
|
|
2437
|
+
const endpoint = typeof summary.endpoint === "string" ? summary.endpoint : void 0;
|
|
2438
|
+
if (!endpoint) return [];
|
|
2439
|
+
const trust = [];
|
|
2440
|
+
if (Array.isArray(summary.trust)) {
|
|
2441
|
+
for (const t of summary.trust) {
|
|
2442
|
+
if (isRecord3(t) && typeof t.type === "string" && typeof t.detail === "string") {
|
|
2443
|
+
trust.push({ type: t.type, detail: t.detail });
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
const si = isRecord3(summary.serverInfo) ? summary.serverInfo : void 0;
|
|
2448
|
+
const name = si && typeof si.name === "string" ? si.name : void 0;
|
|
2449
|
+
return [
|
|
2450
|
+
{
|
|
2451
|
+
id: candidateId(),
|
|
2452
|
+
endpoint,
|
|
2453
|
+
standard: "mcp",
|
|
2454
|
+
origin: event.origin,
|
|
2455
|
+
name,
|
|
2456
|
+
transport: "http",
|
|
2457
|
+
trust,
|
|
2458
|
+
status: event.status,
|
|
2459
|
+
reason: event.reason,
|
|
2460
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2461
|
+
}
|
|
2462
|
+
];
|
|
2463
|
+
}
|
|
2464
|
+
var EXTRACTORS = {
|
|
2465
|
+
ard: extractArd,
|
|
2466
|
+
"ai-catalog": extractAiCatalog,
|
|
2467
|
+
ucp: extractUcp,
|
|
2468
|
+
poppy: extractPoppy,
|
|
2469
|
+
nlweb: extractNlweb,
|
|
2470
|
+
webmcp: extractWebmcp,
|
|
2471
|
+
mcp: extractMcp
|
|
2472
|
+
};
|
|
2473
|
+
function extractCandidates(event) {
|
|
2474
|
+
const extractor = EXTRACTORS[event.standard];
|
|
2475
|
+
if (!extractor) return [];
|
|
2476
|
+
return extractor(event);
|
|
2477
|
+
}
|
|
2478
|
+
function deduplicateCandidates(candidates) {
|
|
2479
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2480
|
+
const result = [];
|
|
2481
|
+
for (const c of candidates) {
|
|
2482
|
+
const key = c.endpoint;
|
|
2483
|
+
if (seen.has(key)) continue;
|
|
2484
|
+
seen.add(key);
|
|
2485
|
+
result.push(c);
|
|
2486
|
+
}
|
|
2487
|
+
return result;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/discovery/inbox.ts
|
|
2491
|
+
function inboxPath(stateDir2) {
|
|
2492
|
+
return `${stateDir2}/inbox.json`;
|
|
2493
|
+
}
|
|
2494
|
+
async function readInbox(fs, stateDir2) {
|
|
2495
|
+
try {
|
|
2496
|
+
const data = await fs.readFile(inboxPath(stateDir2));
|
|
2497
|
+
const parsed = JSON.parse(new TextDecoder().decode(data));
|
|
2498
|
+
if (parsed && Array.isArray(parsed.candidates)) return parsed;
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
return { candidates: [] };
|
|
2502
|
+
}
|
|
2503
|
+
async function writeInbox(fs, stateDir2, inbox) {
|
|
2504
|
+
try {
|
|
2505
|
+
await fs.mkdir(stateDir2);
|
|
2506
|
+
} catch {
|
|
2507
|
+
}
|
|
2508
|
+
const json = JSON.stringify(inbox, null, 2) + "\n";
|
|
2509
|
+
await fs.writeFile(inboxPath(stateDir2), json);
|
|
2510
|
+
}
|
|
2511
|
+
async function appendCandidates(fs, stateDir2, candidates) {
|
|
2512
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
2513
|
+
const existing = new Set(inbox.candidates.map((c) => c.endpoint));
|
|
2514
|
+
for (const c of candidates) {
|
|
2515
|
+
if (existing.has(c.endpoint)) continue;
|
|
2516
|
+
existing.add(c.endpoint);
|
|
2517
|
+
inbox.candidates.push(c);
|
|
2518
|
+
}
|
|
2519
|
+
await writeInbox(fs, stateDir2, inbox);
|
|
2520
|
+
}
|
|
2521
|
+
async function listCandidates(fs, stateDir2) {
|
|
2522
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
2523
|
+
return inbox.candidates.slice().reverse();
|
|
2524
|
+
}
|
|
2525
|
+
async function getCandidate(fs, stateDir2, id) {
|
|
2526
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
2527
|
+
return inbox.candidates.find((c) => c.id === id);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
// src/discovery/probe.ts
|
|
2531
|
+
function isJsonContentType(ct) {
|
|
2532
|
+
if (!ct) return false;
|
|
2533
|
+
const lower = ct.toLowerCase();
|
|
2534
|
+
if (lower.includes("text/html")) return false;
|
|
2535
|
+
return lower.includes("json") || lower.includes("+json");
|
|
2536
|
+
}
|
|
2537
|
+
function parseLinkHeaders(headers) {
|
|
2538
|
+
const links = [];
|
|
2539
|
+
const raw = headers.get("link");
|
|
2540
|
+
if (!raw) return links;
|
|
2541
|
+
const parts = raw.split(/,(?=\s*<)/);
|
|
2542
|
+
for (const part of parts) {
|
|
2543
|
+
const urlMatch = /<([^>]+)>/.exec(part);
|
|
2544
|
+
const relMatch = /rel\s*=\s*"?([^";,\s]+)"?/i.exec(part);
|
|
2545
|
+
if (urlMatch && relMatch) {
|
|
2546
|
+
links.push({ url: urlMatch[1], rel: relMatch[1].toLowerCase() });
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return links;
|
|
2550
|
+
}
|
|
2551
|
+
function wasRedirected(requestUrl, resp) {
|
|
2552
|
+
if (resp.redirected) return true;
|
|
2553
|
+
if (resp.url && resp.url !== requestUrl) return true;
|
|
2554
|
+
return false;
|
|
2555
|
+
}
|
|
2556
|
+
async function probeUrl(fetcher, url, accept) {
|
|
2557
|
+
let resp;
|
|
2558
|
+
try {
|
|
2559
|
+
resp = await fetcher(url, {
|
|
2560
|
+
redirect: "manual",
|
|
2561
|
+
headers: accept ? { Accept: accept } : void 0,
|
|
2562
|
+
credentials: "omit"
|
|
2563
|
+
});
|
|
2564
|
+
} catch {
|
|
2565
|
+
try {
|
|
2566
|
+
resp = await fetcher(url, {
|
|
2567
|
+
redirect: "follow",
|
|
2568
|
+
headers: accept ? { Accept: accept } : void 0,
|
|
2569
|
+
credentials: "omit"
|
|
2570
|
+
});
|
|
2571
|
+
if (wasRedirected(url, resp)) {
|
|
2572
|
+
return { ok: false, status: 301, headers: resp.headers, body: "" };
|
|
2573
|
+
}
|
|
2574
|
+
} catch {
|
|
2575
|
+
return void 0;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
if (resp.status !== 200)
|
|
2579
|
+
return { ok: false, status: resp.status, headers: resp.headers, body: "" };
|
|
2580
|
+
const ct = resp.headers.get("content-type");
|
|
2581
|
+
if (!isJsonContentType(ct))
|
|
2582
|
+
return { ok: false, status: resp.status, headers: resp.headers, body: "" };
|
|
2583
|
+
const body = await resp.text();
|
|
2584
|
+
return { ok: true, status: 200, headers: resp.headers, body };
|
|
2585
|
+
}
|
|
2586
|
+
function isRecord4(v) {
|
|
2587
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2588
|
+
}
|
|
2589
|
+
function makeEvent(standard, origin, url, via, page, status, summary, reason) {
|
|
2590
|
+
return {
|
|
2591
|
+
v: 1,
|
|
2592
|
+
standard,
|
|
2593
|
+
origin,
|
|
2594
|
+
url,
|
|
2595
|
+
via,
|
|
2596
|
+
page,
|
|
2597
|
+
targetId: "",
|
|
2598
|
+
status,
|
|
2599
|
+
summary,
|
|
2600
|
+
...reason ? { reason } : {}
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function tryParseJson(body) {
|
|
2604
|
+
try {
|
|
2605
|
+
return JSON.parse(body);
|
|
2606
|
+
} catch {
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
function classifyArd(origin, url, via, page, body) {
|
|
2611
|
+
const parsed = tryParseJson(body);
|
|
2612
|
+
if (!isRecord4(parsed))
|
|
2613
|
+
return makeEvent("ard", origin, url, via, page, "invalid", null, "invalid JSON");
|
|
2614
|
+
if (!Array.isArray(parsed.entries))
|
|
2615
|
+
return makeEvent("ard", origin, url, via, page, "invalid", null, "missing entries[]");
|
|
2616
|
+
const entries = [];
|
|
2617
|
+
for (const entry of parsed.entries) {
|
|
2618
|
+
if (!isRecord4(entry)) continue;
|
|
2619
|
+
entries.push({
|
|
2620
|
+
identifier: entry.identifier,
|
|
2621
|
+
displayName: entry.displayName,
|
|
2622
|
+
type: entry.type,
|
|
2623
|
+
url: entry.url,
|
|
2624
|
+
capabilities: entry.capabilities,
|
|
2625
|
+
representativeQueries: entry.representativeQueries,
|
|
2626
|
+
trustManifest: entry.trustManifest
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
return makeEvent("ard", origin, url, via, page, "valid", { entries });
|
|
2630
|
+
}
|
|
2631
|
+
function classifyAiCatalog(origin, url, via, page, body) {
|
|
2632
|
+
const parsed = tryParseJson(body);
|
|
2633
|
+
if (!isRecord4(parsed))
|
|
2634
|
+
return makeEvent("ai-catalog", origin, url, via, page, "invalid", null, "invalid JSON");
|
|
2635
|
+
const entries = [];
|
|
2636
|
+
const items = Array.isArray(parsed.entries) ? parsed.entries : Array.isArray(parsed.items) ? parsed.items : void 0;
|
|
2637
|
+
if (!items)
|
|
2638
|
+
return makeEvent("ai-catalog", origin, url, via, page, "invalid", null, "no entries or items");
|
|
2639
|
+
for (const entry of items) {
|
|
2640
|
+
if (!isRecord4(entry)) continue;
|
|
2641
|
+
entries.push({
|
|
2642
|
+
displayName: entry.displayName ?? entry.name,
|
|
2643
|
+
type: entry.type,
|
|
2644
|
+
url: entry.url
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
return makeEvent("ai-catalog", origin, url, via, page, "valid", { entries });
|
|
2648
|
+
}
|
|
2649
|
+
async function probeWellKnown(fetcher, origin, page) {
|
|
2650
|
+
const events = [];
|
|
2651
|
+
const ardUrl = `${origin}/.well-known/ard.json`;
|
|
2652
|
+
const ardResult = await probeUrl(fetcher, ardUrl, "application/json");
|
|
2653
|
+
if (ardResult?.ok) {
|
|
2654
|
+
const ev = classifyArd(origin, ardUrl, "well-known", page, ardResult.body);
|
|
2655
|
+
if (ev) events.push(ev);
|
|
2656
|
+
}
|
|
2657
|
+
const aiCatUrl = `${origin}/.well-known/ai-catalog.json`;
|
|
2658
|
+
const aiCatResult = await probeUrl(fetcher, aiCatUrl, "application/json");
|
|
2659
|
+
if (aiCatResult?.ok) {
|
|
2660
|
+
const ev = classifyAiCatalog(origin, aiCatUrl, "well-known", page, aiCatResult.body);
|
|
2661
|
+
if (ev) events.push(ev);
|
|
2662
|
+
}
|
|
2663
|
+
return events;
|
|
2664
|
+
}
|
|
2665
|
+
async function probeLinkHeaders(fetcher, targetUrl, origin, page) {
|
|
2666
|
+
const events = [];
|
|
2667
|
+
const targetResult = await probeUrl(fetcher, targetUrl);
|
|
2668
|
+
if (!targetResult) return events;
|
|
2669
|
+
const links = parseLinkHeaders(targetResult.headers);
|
|
2670
|
+
for (const link of links) {
|
|
2671
|
+
const resolved = new URL(link.url, targetUrl).href;
|
|
2672
|
+
const classifier = link.rel === "ard" ? classifyArd : link.rel === "ai-catalog" ? classifyAiCatalog : null;
|
|
2673
|
+
if (!classifier) continue;
|
|
2674
|
+
const linkResult = await probeUrl(fetcher, resolved, "application/json");
|
|
2675
|
+
if (linkResult?.ok) {
|
|
2676
|
+
const ev = classifier(origin, resolved, "link-header", page, linkResult.body);
|
|
2677
|
+
if (ev) events.push(ev);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return events;
|
|
2681
|
+
}
|
|
2682
|
+
function mcpJsonRpc(method, params, id = 1) {
|
|
2683
|
+
return JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
2684
|
+
}
|
|
2685
|
+
function parseServerInfo(result) {
|
|
2686
|
+
const si = isRecord4(result.serverInfo) ? result.serverInfo : void 0;
|
|
2687
|
+
if (!si) return void 0;
|
|
2688
|
+
return {
|
|
2689
|
+
name: typeof si.name === "string" ? si.name : void 0,
|
|
2690
|
+
version: typeof si.version === "string" ? si.version : void 0
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
function checkAuthResponse(resp) {
|
|
2694
|
+
if (resp.status !== 401) return void 0;
|
|
2695
|
+
const wwwAuth = resp.headers.get("www-authenticate");
|
|
2696
|
+
if (!wwwAuth) return void 0;
|
|
2697
|
+
return { type: "auth-required", detail: wwwAuth.split(",")[0].trim() };
|
|
2698
|
+
}
|
|
2699
|
+
async function tryMcpPost(fetcher, url, body, extraHeaders) {
|
|
2700
|
+
const headers = {
|
|
2701
|
+
"Content-Type": "application/json",
|
|
2702
|
+
Accept: "application/json",
|
|
2703
|
+
...extraHeaders
|
|
2704
|
+
};
|
|
2705
|
+
try {
|
|
2706
|
+
const resp = await fetcher(url, {
|
|
2707
|
+
method: "POST",
|
|
2708
|
+
headers,
|
|
2709
|
+
body,
|
|
2710
|
+
redirect: "manual",
|
|
2711
|
+
credentials: "omit"
|
|
2712
|
+
});
|
|
2713
|
+
const authSignal = checkAuthResponse(resp);
|
|
2714
|
+
if (authSignal) return { url, trust: [authSignal] };
|
|
2715
|
+
if (resp.status === 200) {
|
|
2716
|
+
const text = await resp.text();
|
|
2717
|
+
const parsed = tryParseJson(text);
|
|
2718
|
+
if (isRecord4(parsed) && isRecord4(parsed.result)) {
|
|
2719
|
+
return { url, serverInfo: parseServerInfo(parsed.result), trust: [] };
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
} catch {
|
|
2723
|
+
}
|
|
2724
|
+
return void 0;
|
|
2725
|
+
}
|
|
2726
|
+
async function probeMcpEndpoint(fetcher, url) {
|
|
2727
|
+
const discoverBody = mcpJsonRpc("server/discover", {
|
|
2728
|
+
protocolVersion: "2026-07-28",
|
|
2729
|
+
clientInfo: { name: "mcpcat", version: "0.0.0" }
|
|
2730
|
+
});
|
|
2731
|
+
const hit = await tryMcpPost(fetcher, url, discoverBody, {
|
|
2732
|
+
"Mcp-Method": "server/discover"
|
|
2733
|
+
});
|
|
2734
|
+
if (hit) return hit;
|
|
2735
|
+
const initBody = mcpJsonRpc("initialize", {
|
|
2736
|
+
protocolVersion: "2025-11-25",
|
|
2737
|
+
clientInfo: { name: "mcpcat", version: "0.0.0" },
|
|
2738
|
+
capabilities: {}
|
|
2739
|
+
});
|
|
2740
|
+
return tryMcpPost(fetcher, url, initBody);
|
|
2741
|
+
}
|
|
2742
|
+
async function probeMcp(fetcher, origin, page, targetUrl) {
|
|
2743
|
+
const events = [];
|
|
2744
|
+
const urls = [targetUrl];
|
|
2745
|
+
const mcpUrl = `${origin}/mcp`;
|
|
2746
|
+
if (mcpUrl !== targetUrl) urls.push(mcpUrl);
|
|
2747
|
+
for (const url of urls) {
|
|
2748
|
+
const hit = await probeMcpEndpoint(fetcher, url);
|
|
2749
|
+
if (hit) {
|
|
2750
|
+
const summary = { endpoint: hit.url };
|
|
2751
|
+
if (hit.serverInfo) summary.serverInfo = hit.serverInfo;
|
|
2752
|
+
if (hit.trust.length > 0) summary.trust = hit.trust;
|
|
2753
|
+
events.push(makeEvent("mcp", origin, hit.url, "well-known", page, "valid", summary));
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
return events;
|
|
2757
|
+
}
|
|
2758
|
+
async function probeDiscover(platform, targetUrl) {
|
|
2759
|
+
let origin;
|
|
2760
|
+
try {
|
|
2761
|
+
const u = new URL(targetUrl);
|
|
2762
|
+
origin = u.origin;
|
|
2763
|
+
} catch {
|
|
2764
|
+
return { events: [] };
|
|
2765
|
+
}
|
|
2766
|
+
const page = targetUrl;
|
|
2767
|
+
const fetcher = platform.fetch;
|
|
2768
|
+
const events = [
|
|
2769
|
+
...await probeWellKnown(fetcher, origin, page),
|
|
2770
|
+
...await probeLinkHeaders(fetcher, targetUrl, origin, page),
|
|
2771
|
+
...await probeMcp(fetcher, origin, page, targetUrl)
|
|
2772
|
+
];
|
|
2773
|
+
return { events };
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// src/discovery/verbs.ts
|
|
2777
|
+
async function readStdin(platform) {
|
|
2778
|
+
const reader = platform.stdin.getReader();
|
|
2779
|
+
const chunks = [];
|
|
2780
|
+
for (; ; ) {
|
|
2781
|
+
const { done, value } = await reader.read();
|
|
2782
|
+
if (done) break;
|
|
2783
|
+
chunks.push(value);
|
|
2784
|
+
}
|
|
2785
|
+
reader.releaseLock();
|
|
2786
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
2787
|
+
const merged = new Uint8Array(total);
|
|
2788
|
+
let offset = 0;
|
|
2789
|
+
for (const c of chunks) {
|
|
2790
|
+
merged.set(c, offset);
|
|
2791
|
+
offset += c.length;
|
|
2792
|
+
}
|
|
2793
|
+
return new TextDecoder().decode(merged);
|
|
2794
|
+
}
|
|
2795
|
+
function parseEvents(text) {
|
|
2796
|
+
const lines = text.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
2797
|
+
const events = [];
|
|
2798
|
+
for (const line of lines) {
|
|
2799
|
+
let parsed;
|
|
2800
|
+
try {
|
|
2801
|
+
parsed = JSON.parse(line);
|
|
2802
|
+
} catch {
|
|
2803
|
+
return { error: `invalid JSON: ${line.slice(0, 80)}`, code: 2 };
|
|
2804
|
+
}
|
|
2805
|
+
const ev = parsed;
|
|
2806
|
+
if (typeof ev.v !== "number") return { error: "missing event version", code: 2 };
|
|
2807
|
+
if (ev.v !== 1) return { error: `unknown event version: ${ev.v}`, code: 2 };
|
|
2808
|
+
events.push(ev);
|
|
2809
|
+
}
|
|
2810
|
+
return events;
|
|
2811
|
+
}
|
|
2812
|
+
function formatCandidate(c) {
|
|
2813
|
+
const parts = [`#${c.id} ${c.standard} ${c.endpoint}`];
|
|
2814
|
+
if (c.name) parts[0] += ` (${c.name})`;
|
|
2815
|
+
if (c.status === "invalid") parts.push(` status: invalid \u2013 ${c.reason ?? "unknown"}`);
|
|
2816
|
+
if (c.suggestion) parts.push(` suggestion: ${c.suggestion}`);
|
|
2817
|
+
for (const t of c.trust) {
|
|
2818
|
+
const icon = t.type.includes("mismatch") ? "\u26A0" : "\u2713";
|
|
2819
|
+
parts.push(` ${icon} ${t.type}: ${t.detail}`);
|
|
2820
|
+
}
|
|
2821
|
+
return parts.join("\n");
|
|
2822
|
+
}
|
|
2823
|
+
function formatCandidateJson(c) {
|
|
2824
|
+
return {
|
|
2825
|
+
id: c.id,
|
|
2826
|
+
endpoint: c.endpoint,
|
|
2827
|
+
standard: c.standard,
|
|
2828
|
+
origin: c.origin,
|
|
2829
|
+
name: c.name,
|
|
2830
|
+
transport: c.transport,
|
|
2831
|
+
trust: c.trust,
|
|
2832
|
+
status: c.status,
|
|
2833
|
+
reason: c.reason,
|
|
2834
|
+
suggestion: c.suggestion,
|
|
2835
|
+
discoveredAt: c.discoveredAt
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
async function runFromDiscovery(platform, args) {
|
|
2839
|
+
let source = "-";
|
|
2840
|
+
for (const arg of args) {
|
|
2841
|
+
if (arg !== "--from-discovery" && !arg.startsWith("-")) {
|
|
2842
|
+
source = arg;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
let text;
|
|
2846
|
+
if (source === "-") {
|
|
2847
|
+
text = await readStdin(platform);
|
|
2848
|
+
} else {
|
|
2849
|
+
try {
|
|
2850
|
+
const data = await platform.fs.readFile(source);
|
|
2851
|
+
text = new TextDecoder().decode(data);
|
|
2852
|
+
} catch {
|
|
2853
|
+
await platform.write(2, `mcpcat add --from-discovery: cannot read '${source}'
|
|
2854
|
+
`);
|
|
2855
|
+
return 2;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
const result = parseEvents(text);
|
|
2859
|
+
if ("error" in result) {
|
|
2860
|
+
await platform.write(2, `mcpcat add --from-discovery: ${result.error}
|
|
2861
|
+
`);
|
|
2862
|
+
return result.code;
|
|
2863
|
+
}
|
|
2864
|
+
resetIdCounter();
|
|
2865
|
+
let allCandidates = [];
|
|
2866
|
+
for (const event of result) {
|
|
2867
|
+
allCandidates.push(...extractCandidates(event));
|
|
2868
|
+
}
|
|
2869
|
+
allCandidates = deduplicateCandidates(allCandidates);
|
|
2870
|
+
if (allCandidates.length === 0) {
|
|
2871
|
+
await platform.write(1, "no MCP candidates found\n");
|
|
2872
|
+
return 0;
|
|
2873
|
+
}
|
|
2874
|
+
const sd = stateDir(platform);
|
|
2875
|
+
await appendCandidates(platform.fs, sd, allCandidates);
|
|
2876
|
+
for (const c of allCandidates) {
|
|
2877
|
+
await platform.write(1, `${formatCandidate(c)}
|
|
2878
|
+
`);
|
|
2879
|
+
}
|
|
2880
|
+
return 0;
|
|
2881
|
+
}
|
|
2882
|
+
async function runInbox(platform, args) {
|
|
2883
|
+
const json = args.includes("--json");
|
|
2884
|
+
const sd = stateDir(platform);
|
|
2885
|
+
const candidates = await listCandidates(platform.fs, sd);
|
|
2886
|
+
if (candidates.length === 0) {
|
|
2887
|
+
await platform.write(1, "inbox is empty\n");
|
|
2888
|
+
return 0;
|
|
2889
|
+
}
|
|
2890
|
+
if (json) {
|
|
2891
|
+
await platform.write(1, JSON.stringify(candidates.map(formatCandidateJson), null, 2) + "\n");
|
|
2892
|
+
} else {
|
|
2893
|
+
for (const c of candidates) {
|
|
2894
|
+
await platform.write(1, `${formatCandidate(c)}
|
|
2895
|
+
`);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return 0;
|
|
1820
2899
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2900
|
+
async function runAddCandidate(platform, name, candidateId2, configFile) {
|
|
2901
|
+
const sd = stateDir(platform);
|
|
2902
|
+
const candidate = await getCandidate(platform.fs, sd, candidateId2);
|
|
2903
|
+
if (!candidate) {
|
|
2904
|
+
await platform.write(2, `mcpcat add: candidate #${candidateId2} not found in inbox
|
|
2905
|
+
`);
|
|
2906
|
+
return 1;
|
|
2907
|
+
}
|
|
2908
|
+
if (candidate.transport === "webmcp") {
|
|
2909
|
+
await platform.write(
|
|
2910
|
+
2,
|
|
2911
|
+
`mcpcat add: WebMCP candidates cannot be added directly; use: ${candidate.suggestion}
|
|
2912
|
+
`
|
|
2913
|
+
);
|
|
2914
|
+
return 2;
|
|
2915
|
+
}
|
|
2916
|
+
const path = configFile ?? userConfigPath(platform);
|
|
2917
|
+
const config = { url: candidate.endpoint };
|
|
2918
|
+
const { replaced } = await addServer(platform.fs, path, name, config);
|
|
2919
|
+
for (const t of candidate.trust) {
|
|
2920
|
+
if (t.type.includes("mismatch")) {
|
|
2921
|
+
await platform.write(2, `mcpcat add: warning: ${t.type}: ${t.detail}
|
|
2922
|
+
`);
|
|
1831
2923
|
}
|
|
1832
2924
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
2925
|
+
await platform.write(1, `${replaced ? "replaced" : "added"} ${name} in ${path}
|
|
2926
|
+
`);
|
|
2927
|
+
return 0;
|
|
2928
|
+
}
|
|
2929
|
+
async function runDiscover(platform, args) {
|
|
2930
|
+
const positional = [];
|
|
2931
|
+
let json = false;
|
|
2932
|
+
for (const arg of args) {
|
|
2933
|
+
if (arg === "--json") {
|
|
2934
|
+
json = true;
|
|
2935
|
+
} else if (!arg.startsWith("-")) {
|
|
2936
|
+
positional.push(arg);
|
|
1839
2937
|
}
|
|
1840
|
-
return void 0;
|
|
1841
2938
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
2939
|
+
if (positional.length < 1) {
|
|
2940
|
+
await platform.write(2, "mcpcat discover: requires a URL\n");
|
|
2941
|
+
return 2;
|
|
1844
2942
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
2943
|
+
const url = positional[0];
|
|
2944
|
+
if (!URL.canParse(url)) {
|
|
2945
|
+
await platform.write(2, `mcpcat discover: '${url}' is not a valid URL
|
|
2946
|
+
`);
|
|
2947
|
+
return 2;
|
|
1847
2948
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
projectPath,
|
|
1854
|
-
trustProject: options?.trustProject ?? false,
|
|
1855
|
-
fs: platform.fs
|
|
1856
|
-
});
|
|
1857
|
-
return new _MergedConfigSource(config);
|
|
2949
|
+
const { events } = await probeDiscover(platform, url);
|
|
2950
|
+
resetIdCounter();
|
|
2951
|
+
let allCandidates = [];
|
|
2952
|
+
for (const event of events) {
|
|
2953
|
+
allCandidates.push(...extractCandidates(event));
|
|
1858
2954
|
}
|
|
1859
|
-
|
|
2955
|
+
allCandidates = deduplicateCandidates(allCandidates);
|
|
2956
|
+
if (allCandidates.length > 0) {
|
|
2957
|
+
const sd = stateDir(platform);
|
|
2958
|
+
await appendCandidates(platform.fs, sd, allCandidates);
|
|
2959
|
+
}
|
|
2960
|
+
if (allCandidates.length === 0) {
|
|
2961
|
+
await platform.write(1, "no MCP candidates found\n");
|
|
2962
|
+
return 0;
|
|
2963
|
+
}
|
|
2964
|
+
if (json) {
|
|
2965
|
+
await platform.write(1, JSON.stringify(allCandidates.map(formatCandidateJson), null, 2) + "\n");
|
|
2966
|
+
} else {
|
|
2967
|
+
for (const c of allCandidates) {
|
|
2968
|
+
await platform.write(1, `${formatCandidate(c)}
|
|
2969
|
+
`);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
return 0;
|
|
2973
|
+
}
|
|
1860
2974
|
|
|
1861
2975
|
// src/config/expand.ts
|
|
1862
2976
|
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
@@ -3251,158 +4365,12 @@ async function handleMcpRequest(request, tools, call, serverName, version, allow
|
|
|
3251
4365
|
};
|
|
3252
4366
|
}
|
|
3253
4367
|
|
|
3254
|
-
// src/config/lock.ts
|
|
3255
|
-
var LOCK_STALE_MS = 1e4;
|
|
3256
|
-
var LOCK_POLL_MS = 50;
|
|
3257
|
-
var LOCK_MAX_WAIT_MS = 5e3;
|
|
3258
|
-
function isEexist(err) {
|
|
3259
|
-
if (err instanceof Error && "code" in err)
|
|
3260
|
-
return err.code === "EEXIST";
|
|
3261
|
-
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
3262
|
-
return false;
|
|
3263
|
-
}
|
|
3264
|
-
async function removeStaleLock(fs, lockPath) {
|
|
3265
|
-
try {
|
|
3266
|
-
const s = await fs.stat(lockPath);
|
|
3267
|
-
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
3268
|
-
try {
|
|
3269
|
-
await fs.rmdir(lockPath);
|
|
3270
|
-
} catch {
|
|
3271
|
-
}
|
|
3272
|
-
return true;
|
|
3273
|
-
}
|
|
3274
|
-
} catch {
|
|
3275
|
-
}
|
|
3276
|
-
return false;
|
|
3277
|
-
}
|
|
3278
|
-
async function acquireLock(fs, configPath) {
|
|
3279
|
-
const lockPath = `${configPath}.lock`;
|
|
3280
|
-
const start = Date.now();
|
|
3281
|
-
while (true) {
|
|
3282
|
-
try {
|
|
3283
|
-
await fs.mkdirExclusive(lockPath);
|
|
3284
|
-
break;
|
|
3285
|
-
} catch (err) {
|
|
3286
|
-
if (!isEexist(err)) throw err;
|
|
3287
|
-
await removeStaleLock(fs, lockPath);
|
|
3288
|
-
}
|
|
3289
|
-
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
3290
|
-
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
3291
|
-
}
|
|
3292
|
-
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
3293
|
-
}
|
|
3294
|
-
let released = false;
|
|
3295
|
-
return async () => {
|
|
3296
|
-
if (released) return;
|
|
3297
|
-
released = true;
|
|
3298
|
-
try {
|
|
3299
|
-
await fs.rmdir(lockPath);
|
|
3300
|
-
} catch {
|
|
3301
|
-
}
|
|
3302
|
-
};
|
|
3303
|
-
}
|
|
3304
|
-
|
|
3305
|
-
// src/config/edit.ts
|
|
3306
|
-
function isRecord2(value) {
|
|
3307
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3308
|
-
}
|
|
3309
|
-
function detectIndent(text) {
|
|
3310
|
-
const match = /^([ \t]+)\S/m.exec(text);
|
|
3311
|
-
return match ? match[1] : " ";
|
|
3312
|
-
}
|
|
3313
|
-
async function readText(fs, path) {
|
|
3314
|
-
try {
|
|
3315
|
-
const data = await fs.readFile(path);
|
|
3316
|
-
return new TextDecoder().decode(data);
|
|
3317
|
-
} catch {
|
|
3318
|
-
return void 0;
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
async function editMcpServers(fs, path, edit) {
|
|
3322
|
-
const release = await acquireLock(fs, path);
|
|
3323
|
-
try {
|
|
3324
|
-
const text = await readText(fs, path);
|
|
3325
|
-
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
3326
|
-
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
3327
|
-
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
3328
|
-
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
3329
|
-
if (!edit(servers, parsed)) return { changed: false };
|
|
3330
|
-
const indent = text ? detectIndent(text) : " ";
|
|
3331
|
-
const output = `${JSON.stringify(parsed, null, indent)}
|
|
3332
|
-
`;
|
|
3333
|
-
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
3334
|
-
if (dir) {
|
|
3335
|
-
try {
|
|
3336
|
-
await fs.mkdir(dir);
|
|
3337
|
-
} catch {
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
3341
|
-
await fs.writeFile(tmpPath, output);
|
|
3342
|
-
await fs.rename(tmpPath, path);
|
|
3343
|
-
return { changed: true };
|
|
3344
|
-
} finally {
|
|
3345
|
-
await release();
|
|
3346
|
-
}
|
|
3347
|
-
}
|
|
3348
|
-
async function addServer(fs, path, name, config) {
|
|
3349
|
-
let replaced = false;
|
|
3350
|
-
await editMcpServers(fs, path, (servers, parsed) => {
|
|
3351
|
-
const target = servers ?? {};
|
|
3352
|
-
replaced = target[name] !== void 0;
|
|
3353
|
-
target[name] = config;
|
|
3354
|
-
parsed.mcpServers = target;
|
|
3355
|
-
return true;
|
|
3356
|
-
});
|
|
3357
|
-
return { replaced };
|
|
3358
|
-
}
|
|
3359
|
-
async function removeServer(fs, path, name) {
|
|
3360
|
-
let removed = false;
|
|
3361
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3362
|
-
if (!servers || servers[name] === void 0) return false;
|
|
3363
|
-
delete servers[name];
|
|
3364
|
-
removed = true;
|
|
3365
|
-
return true;
|
|
3366
|
-
});
|
|
3367
|
-
return removed;
|
|
3368
|
-
}
|
|
3369
|
-
async function updateServer(fs, path, name, patch) {
|
|
3370
|
-
let updated = false;
|
|
3371
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3372
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
3373
|
-
const entry = servers[name];
|
|
3374
|
-
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
3375
|
-
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
3376
|
-
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
3377
|
-
updated = true;
|
|
3378
|
-
return true;
|
|
3379
|
-
});
|
|
3380
|
-
return updated;
|
|
3381
|
-
}
|
|
3382
|
-
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
3383
|
-
let updated = false;
|
|
3384
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3385
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
3386
|
-
const entry = servers[name];
|
|
3387
|
-
if (toolGlob) {
|
|
3388
|
-
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
3389
|
-
te[toolGlob] = exposure;
|
|
3390
|
-
entry.toolExposure = te;
|
|
3391
|
-
} else {
|
|
3392
|
-
entry.exposure = exposure;
|
|
3393
|
-
}
|
|
3394
|
-
updated = true;
|
|
3395
|
-
return true;
|
|
3396
|
-
});
|
|
3397
|
-
return updated;
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
4368
|
// src/config/import.ts
|
|
3401
|
-
function
|
|
4369
|
+
function isRecord5(value) {
|
|
3402
4370
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3403
4371
|
}
|
|
3404
4372
|
function isStringRecord2(value) {
|
|
3405
|
-
return
|
|
4373
|
+
return isRecord5(value) && Object.values(value).every((v) => typeof v === "string");
|
|
3406
4374
|
}
|
|
3407
4375
|
function tryParse(text) {
|
|
3408
4376
|
try {
|
|
@@ -3432,17 +4400,17 @@ function importClaude(text, names) {
|
|
|
3432
4400
|
result.errors.push(error);
|
|
3433
4401
|
return result;
|
|
3434
4402
|
}
|
|
3435
|
-
if (!
|
|
4403
|
+
if (!isRecord5(parsed)) {
|
|
3436
4404
|
result.errors.push("expected an object");
|
|
3437
4405
|
return result;
|
|
3438
4406
|
}
|
|
3439
4407
|
const mcpServers = parsed.mcpServers;
|
|
3440
|
-
if (!
|
|
4408
|
+
if (!isRecord5(mcpServers)) {
|
|
3441
4409
|
result.errors.push('no "mcpServers" object');
|
|
3442
4410
|
return result;
|
|
3443
4411
|
}
|
|
3444
4412
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
3445
|
-
if (!
|
|
4413
|
+
if (!isRecord5(entry)) {
|
|
3446
4414
|
result.errors.push(`${name}: not an object`);
|
|
3447
4415
|
continue;
|
|
3448
4416
|
}
|
|
@@ -3457,17 +4425,17 @@ function importVSCode(text, names) {
|
|
|
3457
4425
|
result.errors.push(error);
|
|
3458
4426
|
return result;
|
|
3459
4427
|
}
|
|
3460
|
-
if (!
|
|
4428
|
+
if (!isRecord5(parsed)) {
|
|
3461
4429
|
result.errors.push("expected an object");
|
|
3462
4430
|
return result;
|
|
3463
4431
|
}
|
|
3464
4432
|
const servers = parsed.servers ?? parsed.mcpServers;
|
|
3465
|
-
if (!
|
|
4433
|
+
if (!isRecord5(servers)) {
|
|
3466
4434
|
result.errors.push('no "servers" or "mcpServers" object');
|
|
3467
4435
|
return result;
|
|
3468
4436
|
}
|
|
3469
4437
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
3470
|
-
if (!
|
|
4438
|
+
if (!isRecord5(entry)) {
|
|
3471
4439
|
result.errors.push(`${name}: not an object`);
|
|
3472
4440
|
continue;
|
|
3473
4441
|
}
|
|
@@ -3501,17 +4469,17 @@ function importSixServers(text, names) {
|
|
|
3501
4469
|
result.errors.push(error);
|
|
3502
4470
|
return result;
|
|
3503
4471
|
}
|
|
3504
|
-
if (!
|
|
4472
|
+
if (!isRecord5(parsed)) {
|
|
3505
4473
|
result.errors.push("expected an object");
|
|
3506
4474
|
return result;
|
|
3507
4475
|
}
|
|
3508
4476
|
const servers = parsed.servers;
|
|
3509
|
-
if (!
|
|
4477
|
+
if (!isRecord5(servers)) {
|
|
3510
4478
|
result.errors.push('no "servers" object');
|
|
3511
4479
|
return result;
|
|
3512
4480
|
}
|
|
3513
4481
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
3514
|
-
if (!
|
|
4482
|
+
if (!isRecord5(entry)) {
|
|
3515
4483
|
result.errors.push(`${name}: not an object`);
|
|
3516
4484
|
continue;
|
|
3517
4485
|
}
|
|
@@ -3519,7 +4487,7 @@ function importSixServers(text, names) {
|
|
|
3519
4487
|
if (typeof entry.url === "string") config.url = entry.url;
|
|
3520
4488
|
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
3521
4489
|
if (typeof entry.exposure === "string") config.exposure = entry.exposure;
|
|
3522
|
-
if (
|
|
4490
|
+
if (isRecord5(entry.toolExposure)) config.toolExposure = entry.toolExposure;
|
|
3523
4491
|
result.servers.push({ name, config });
|
|
3524
4492
|
}
|
|
3525
4493
|
return result;
|
|
@@ -3536,7 +4504,7 @@ function importCodexToml(text, names) {
|
|
|
3536
4504
|
if (fields.command) config.command = fields.command;
|
|
3537
4505
|
if (fields.args) config.args = fields.args;
|
|
3538
4506
|
if (fields.url) config.url = fields.url;
|
|
3539
|
-
if (fields.env &&
|
|
4507
|
+
if (fields.env && isRecord5(fields.env)) config.env = fields.env;
|
|
3540
4508
|
result.servers.push({ name, config });
|
|
3541
4509
|
}
|
|
3542
4510
|
return result;
|
|
@@ -3588,7 +4556,7 @@ function convertOpenCodeLocal(entry) {
|
|
|
3588
4556
|
} else if (typeof entry.command === "string") {
|
|
3589
4557
|
config.command = entry.command;
|
|
3590
4558
|
}
|
|
3591
|
-
if (
|
|
4559
|
+
if (isRecord5(entry.environment)) {
|
|
3592
4560
|
const env = {};
|
|
3593
4561
|
for (const [k, v] of Object.entries(entry.environment)) {
|
|
3594
4562
|
if (typeof v === "string")
|
|
@@ -3611,17 +4579,17 @@ function importOpenCode(text, names) {
|
|
|
3611
4579
|
result.errors.push(error);
|
|
3612
4580
|
return result;
|
|
3613
4581
|
}
|
|
3614
|
-
if (!
|
|
4582
|
+
if (!isRecord5(parsed)) {
|
|
3615
4583
|
result.errors.push("expected an object");
|
|
3616
4584
|
return result;
|
|
3617
4585
|
}
|
|
3618
4586
|
const mcpServers = parsed.mcpServers ?? parsed.mcp;
|
|
3619
|
-
if (!
|
|
4587
|
+
if (!isRecord5(mcpServers)) {
|
|
3620
4588
|
result.errors.push("no mcp servers found");
|
|
3621
4589
|
return result;
|
|
3622
4590
|
}
|
|
3623
4591
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
3624
|
-
if (!
|
|
4592
|
+
if (!isRecord5(entry)) {
|
|
3625
4593
|
result.errors.push(`${name}: not an object`);
|
|
3626
4594
|
continue;
|
|
3627
4595
|
}
|
|
@@ -3637,9 +4605,9 @@ function detectFormat(text, filename) {
|
|
|
3637
4605
|
if (filename.includes(".vscode")) return "vscode";
|
|
3638
4606
|
try {
|
|
3639
4607
|
const parsed = JSON.parse(text);
|
|
3640
|
-
if (
|
|
3641
|
-
if (parsed.servers &&
|
|
3642
|
-
if (parsed.servers &&
|
|
4608
|
+
if (isRecord5(parsed)) {
|
|
4609
|
+
if (parsed.servers && isRecord5(parsed.servers) && parsed.version !== void 0) return "six";
|
|
4610
|
+
if (parsed.servers && isRecord5(parsed.servers)) return "vscode";
|
|
3643
4611
|
if (parsed.mcpServers) return "claude";
|
|
3644
4612
|
if (parsed.mcp) return "opencode";
|
|
3645
4613
|
}
|
|
@@ -4208,9 +5176,7 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
4208
5176
|
"task",
|
|
4209
5177
|
"login",
|
|
4210
5178
|
"logout",
|
|
4211
|
-
"code"
|
|
4212
|
-
"discover",
|
|
4213
|
-
"inbox"
|
|
5179
|
+
"code"
|
|
4214
5180
|
]);
|
|
4215
5181
|
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe", "serve"]);
|
|
4216
5182
|
function helpText(version) {
|
|
@@ -4238,8 +5204,8 @@ Configuration verbs:
|
|
|
4238
5204
|
Server and code verbs:
|
|
4239
5205
|
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
4240
5206
|
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
4241
|
-
discover <url> Find MCP servers
|
|
4242
|
-
inbox
|
|
5207
|
+
discover <url> Find MCP servers at a URL
|
|
5208
|
+
inbox [--json] List discovery candidates
|
|
4243
5209
|
help [verb] Show help
|
|
4244
5210
|
|
|
4245
5211
|
Options:
|
|
@@ -4370,6 +5336,21 @@ function renderCallResult(result) {
|
|
|
4370
5336
|
}
|
|
4371
5337
|
return parts.join("\n");
|
|
4372
5338
|
}
|
|
5339
|
+
var NETWORK_HINT = "connection failed (use slicc-node, slicc-swift, or the browser extension for servers without CORS)";
|
|
5340
|
+
function transportHint(err) {
|
|
5341
|
+
if (err instanceof TypeError) {
|
|
5342
|
+
const cause = err.cause;
|
|
5343
|
+
if (cause?.code === "ECONNREFUSED" || cause?.code === "ENETUNREACH") {
|
|
5344
|
+
return `connection failed: ${err.message}`;
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
if (err instanceof Error && "status" in err && err.status === 502) {
|
|
5348
|
+
const msg = err.message;
|
|
5349
|
+
if (msg) return `${NETWORK_HINT}: ${msg}`;
|
|
5350
|
+
return NETWORK_HINT;
|
|
5351
|
+
}
|
|
5352
|
+
return void 0;
|
|
5353
|
+
}
|
|
4373
5354
|
async function readDataArg(platform, dataArg) {
|
|
4374
5355
|
let raw;
|
|
4375
5356
|
if (dataArg.startsWith("@-")) {
|
|
@@ -4539,6 +5520,37 @@ async function dispatchConfigVerb(platform, verb, args) {
|
|
|
4539
5520
|
return 1;
|
|
4540
5521
|
}
|
|
4541
5522
|
}
|
|
5523
|
+
async function dispatchDiscoveryVerb(platform, parsed, args) {
|
|
5524
|
+
if (parsed.verb === "add" && args.includes("--from-discovery")) {
|
|
5525
|
+
return runFromDiscovery(platform, args.slice(1));
|
|
5526
|
+
}
|
|
5527
|
+
if (parsed.verb === "add" && args.includes("--candidate")) {
|
|
5528
|
+
const rest = args.slice(1);
|
|
5529
|
+
const nameIdx = rest.findIndex((a) => !a.startsWith("-") && a !== "--candidate");
|
|
5530
|
+
const candidateIdx = rest.indexOf("--candidate");
|
|
5531
|
+
if (nameIdx < 0 || candidateIdx < 0 || candidateIdx + 1 >= rest.length) {
|
|
5532
|
+
await platform.write(2, "mcpcat add --candidate: requires <name> --candidate <id>\n");
|
|
5533
|
+
return 2;
|
|
5534
|
+
}
|
|
5535
|
+
const name = rest[nameIdx];
|
|
5536
|
+
const candidateId2 = rest[candidateIdx + 1];
|
|
5537
|
+
const configIdx = rest.indexOf("--config");
|
|
5538
|
+
const configFile = configIdx >= 0 && configIdx + 1 < rest.length ? rest[configIdx + 1] : void 0;
|
|
5539
|
+
return runAddCandidate(platform, name, candidateId2, configFile);
|
|
5540
|
+
}
|
|
5541
|
+
if (parsed.verb === "discover") return runDiscover(platform, args.slice(1));
|
|
5542
|
+
if (parsed.verb === "inbox") return runInbox(platform, args.slice(1));
|
|
5543
|
+
return void 0;
|
|
5544
|
+
}
|
|
5545
|
+
async function dispatchServeVerb(platform, args, version) {
|
|
5546
|
+
const serveArgs = args.slice(args.indexOf("serve") + 1);
|
|
5547
|
+
const serveOptions = parseServeFlags(serveArgs);
|
|
5548
|
+
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
5549
|
+
await platform.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
5550
|
+
return 2;
|
|
5551
|
+
}
|
|
5552
|
+
return runServe(platform, serveOptions, version);
|
|
5553
|
+
}
|
|
4542
5554
|
async function run(platform, version, args) {
|
|
4543
5555
|
if (args.includes("--version") || args.includes("-V")) {
|
|
4544
5556
|
await platform.write(1, `${version}
|
|
@@ -4559,21 +5571,19 @@ async function run(platform, version, args) {
|
|
|
4559
5571
|
return 2;
|
|
4560
5572
|
}
|
|
4561
5573
|
if (parsed.verb in CONFIG_VERBS) {
|
|
5574
|
+
const disc = await dispatchDiscoveryVerb(platform, parsed, args);
|
|
5575
|
+
if (disc !== void 0) return disc;
|
|
4562
5576
|
return dispatchConfigVerb(platform, parsed.verb, args);
|
|
4563
5577
|
}
|
|
5578
|
+
const discResult = await dispatchDiscoveryVerb(platform, parsed, args);
|
|
5579
|
+
if (discResult !== void 0) return discResult;
|
|
4564
5580
|
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
4565
5581
|
await platform.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
4566
5582
|
`);
|
|
4567
5583
|
return 1;
|
|
4568
5584
|
}
|
|
4569
5585
|
if (parsed.verb === "serve") {
|
|
4570
|
-
|
|
4571
|
-
const serveOptions = parseServeFlags(serveArgs);
|
|
4572
|
-
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
4573
|
-
await platform.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
4574
|
-
return 2;
|
|
4575
|
-
}
|
|
4576
|
-
return runServe(platform, serveOptions, version);
|
|
5586
|
+
return dispatchServeVerb(platform, args, version);
|
|
4577
5587
|
}
|
|
4578
5588
|
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
4579
5589
|
await platform.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
@@ -4611,7 +5621,8 @@ async function run(platform, version, args) {
|
|
|
4611
5621
|
version
|
|
4612
5622
|
});
|
|
4613
5623
|
} catch (err) {
|
|
4614
|
-
|
|
5624
|
+
const msg = transportHint(err) ?? `connection failed: ${err.message}`;
|
|
5625
|
+
await platform.write(2, `mcpcat: ${msg}
|
|
4615
5626
|
`);
|
|
4616
5627
|
return 3;
|
|
4617
5628
|
}
|
|
@@ -4664,12 +5675,22 @@ async function kernelSpawn(ctx, options) {
|
|
|
4664
5675
|
stdout: "pipe",
|
|
4665
5676
|
stderr: "pipe"
|
|
4666
5677
|
});
|
|
5678
|
+
const childPid = result.pid;
|
|
4667
5679
|
return {
|
|
4668
5680
|
stdin: result.stdin,
|
|
4669
5681
|
stdout: result.stdout,
|
|
4670
5682
|
stderr: result.stderr,
|
|
4671
5683
|
wait: () => result.wait(),
|
|
4672
|
-
kill: (signal) =>
|
|
5684
|
+
kill: (signal) => {
|
|
5685
|
+
try {
|
|
5686
|
+
ctx.kill(-childPid, signal);
|
|
5687
|
+
} catch {
|
|
5688
|
+
}
|
|
5689
|
+
try {
|
|
5690
|
+
ctx.kill(childPid, signal);
|
|
5691
|
+
} catch {
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
4673
5694
|
};
|
|
4674
5695
|
}
|
|
4675
5696
|
async function kernelListen(ctx, options) {
|
|
@@ -4735,7 +5756,7 @@ function createKernelPlatform(ctx) {
|
|
|
4735
5756
|
async function main(ctx) {
|
|
4736
5757
|
const platform = createKernelPlatform(ctx);
|
|
4737
5758
|
const args = ctx.argv.slice(1);
|
|
4738
|
-
return run(platform, "0.
|
|
5759
|
+
return run(platform, "0.6.0", args);
|
|
4739
5760
|
}
|
|
4740
5761
|
export {
|
|
4741
5762
|
main as default
|