@ai-ecoverse/mcpcat 0.5.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 +978 -181
- package/dist/kernel/mcpcat.mjs +978 -181
- package/package.json +3 -3
package/dist/kernel/mcpcat.mjs
CHANGED
|
@@ -1475,10 +1475,10 @@ async function createStatelessConnection(options) {
|
|
|
1475
1475
|
timeoutMs = 6e4,
|
|
1476
1476
|
clientInfo = { name: "mcpcat", version: "0.0.0" }
|
|
1477
1477
|
} = options;
|
|
1478
|
-
let
|
|
1478
|
+
let nextId2 = 1;
|
|
1479
1479
|
let negotiatedVersion;
|
|
1480
1480
|
async function request(method, params) {
|
|
1481
|
-
const id =
|
|
1481
|
+
const id = nextId2++;
|
|
1482
1482
|
const headers = {
|
|
1483
1483
|
"Content-Type": "application/json",
|
|
1484
1484
|
Accept: "application/json, text/event-stream",
|
|
@@ -2016,6 +2016,13 @@ function userConfigPath(platform) {
|
|
|
2016
2016
|
function projectConfigPath(platform) {
|
|
2017
2017
|
return `${platform.cwd}/.pi/mcp.json`;
|
|
2018
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
|
+
}
|
|
2019
2026
|
|
|
2020
2027
|
// src/config/index.ts
|
|
2021
2028
|
var MergedConfigSource = class _MergedConfigSource {
|
|
@@ -2056,6 +2063,915 @@ var MergedConfigSource = class _MergedConfigSource {
|
|
|
2056
2063
|
}
|
|
2057
2064
|
};
|
|
2058
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;
|
|
2899
|
+
}
|
|
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
|
+
`);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
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);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (positional.length < 1) {
|
|
2940
|
+
await platform.write(2, "mcpcat discover: requires a URL\n");
|
|
2941
|
+
return 2;
|
|
2942
|
+
}
|
|
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;
|
|
2948
|
+
}
|
|
2949
|
+
const { events } = await probeDiscover(platform, url);
|
|
2950
|
+
resetIdCounter();
|
|
2951
|
+
let allCandidates = [];
|
|
2952
|
+
for (const event of events) {
|
|
2953
|
+
allCandidates.push(...extractCandidates(event));
|
|
2954
|
+
}
|
|
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
|
+
}
|
|
2974
|
+
|
|
2059
2975
|
// src/config/expand.ts
|
|
2060
2976
|
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2061
2977
|
var PLACEHOLDER_TEST = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
@@ -3449,158 +4365,12 @@ async function handleMcpRequest(request, tools, call, serverName, version, allow
|
|
|
3449
4365
|
};
|
|
3450
4366
|
}
|
|
3451
4367
|
|
|
3452
|
-
// src/config/lock.ts
|
|
3453
|
-
var LOCK_STALE_MS = 1e4;
|
|
3454
|
-
var LOCK_POLL_MS = 50;
|
|
3455
|
-
var LOCK_MAX_WAIT_MS = 5e3;
|
|
3456
|
-
function isEexist(err) {
|
|
3457
|
-
if (err instanceof Error && "code" in err)
|
|
3458
|
-
return err.code === "EEXIST";
|
|
3459
|
-
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
3460
|
-
return false;
|
|
3461
|
-
}
|
|
3462
|
-
async function removeStaleLock(fs, lockPath) {
|
|
3463
|
-
try {
|
|
3464
|
-
const s = await fs.stat(lockPath);
|
|
3465
|
-
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
3466
|
-
try {
|
|
3467
|
-
await fs.rmdir(lockPath);
|
|
3468
|
-
} catch {
|
|
3469
|
-
}
|
|
3470
|
-
return true;
|
|
3471
|
-
}
|
|
3472
|
-
} catch {
|
|
3473
|
-
}
|
|
3474
|
-
return false;
|
|
3475
|
-
}
|
|
3476
|
-
async function acquireLock(fs, configPath) {
|
|
3477
|
-
const lockPath = `${configPath}.lock`;
|
|
3478
|
-
const start = Date.now();
|
|
3479
|
-
while (true) {
|
|
3480
|
-
try {
|
|
3481
|
-
await fs.mkdirExclusive(lockPath);
|
|
3482
|
-
break;
|
|
3483
|
-
} catch (err) {
|
|
3484
|
-
if (!isEexist(err)) throw err;
|
|
3485
|
-
await removeStaleLock(fs, lockPath);
|
|
3486
|
-
}
|
|
3487
|
-
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
3488
|
-
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
3489
|
-
}
|
|
3490
|
-
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
3491
|
-
}
|
|
3492
|
-
let released = false;
|
|
3493
|
-
return async () => {
|
|
3494
|
-
if (released) return;
|
|
3495
|
-
released = true;
|
|
3496
|
-
try {
|
|
3497
|
-
await fs.rmdir(lockPath);
|
|
3498
|
-
} catch {
|
|
3499
|
-
}
|
|
3500
|
-
};
|
|
3501
|
-
}
|
|
3502
|
-
|
|
3503
|
-
// src/config/edit.ts
|
|
3504
|
-
function isRecord2(value) {
|
|
3505
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3506
|
-
}
|
|
3507
|
-
function detectIndent(text) {
|
|
3508
|
-
const match = /^([ \t]+)\S/m.exec(text);
|
|
3509
|
-
return match ? match[1] : " ";
|
|
3510
|
-
}
|
|
3511
|
-
async function readText(fs, path) {
|
|
3512
|
-
try {
|
|
3513
|
-
const data = await fs.readFile(path);
|
|
3514
|
-
return new TextDecoder().decode(data);
|
|
3515
|
-
} catch {
|
|
3516
|
-
return void 0;
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
async function editMcpServers(fs, path, edit) {
|
|
3520
|
-
const release = await acquireLock(fs, path);
|
|
3521
|
-
try {
|
|
3522
|
-
const text = await readText(fs, path);
|
|
3523
|
-
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
3524
|
-
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
3525
|
-
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
3526
|
-
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
3527
|
-
if (!edit(servers, parsed)) return { changed: false };
|
|
3528
|
-
const indent = text ? detectIndent(text) : " ";
|
|
3529
|
-
const output = `${JSON.stringify(parsed, null, indent)}
|
|
3530
|
-
`;
|
|
3531
|
-
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
3532
|
-
if (dir) {
|
|
3533
|
-
try {
|
|
3534
|
-
await fs.mkdir(dir);
|
|
3535
|
-
} catch {
|
|
3536
|
-
}
|
|
3537
|
-
}
|
|
3538
|
-
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
3539
|
-
await fs.writeFile(tmpPath, output);
|
|
3540
|
-
await fs.rename(tmpPath, path);
|
|
3541
|
-
return { changed: true };
|
|
3542
|
-
} finally {
|
|
3543
|
-
await release();
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
async function addServer(fs, path, name, config) {
|
|
3547
|
-
let replaced = false;
|
|
3548
|
-
await editMcpServers(fs, path, (servers, parsed) => {
|
|
3549
|
-
const target = servers ?? {};
|
|
3550
|
-
replaced = target[name] !== void 0;
|
|
3551
|
-
target[name] = config;
|
|
3552
|
-
parsed.mcpServers = target;
|
|
3553
|
-
return true;
|
|
3554
|
-
});
|
|
3555
|
-
return { replaced };
|
|
3556
|
-
}
|
|
3557
|
-
async function removeServer(fs, path, name) {
|
|
3558
|
-
let removed = false;
|
|
3559
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3560
|
-
if (!servers || servers[name] === void 0) return false;
|
|
3561
|
-
delete servers[name];
|
|
3562
|
-
removed = true;
|
|
3563
|
-
return true;
|
|
3564
|
-
});
|
|
3565
|
-
return removed;
|
|
3566
|
-
}
|
|
3567
|
-
async function updateServer(fs, path, name, patch) {
|
|
3568
|
-
let updated = false;
|
|
3569
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3570
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
3571
|
-
const entry = servers[name];
|
|
3572
|
-
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
3573
|
-
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
3574
|
-
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
3575
|
-
updated = true;
|
|
3576
|
-
return true;
|
|
3577
|
-
});
|
|
3578
|
-
return updated;
|
|
3579
|
-
}
|
|
3580
|
-
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
3581
|
-
let updated = false;
|
|
3582
|
-
await editMcpServers(fs, path, (servers) => {
|
|
3583
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
3584
|
-
const entry = servers[name];
|
|
3585
|
-
if (toolGlob) {
|
|
3586
|
-
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
3587
|
-
te[toolGlob] = exposure;
|
|
3588
|
-
entry.toolExposure = te;
|
|
3589
|
-
} else {
|
|
3590
|
-
entry.exposure = exposure;
|
|
3591
|
-
}
|
|
3592
|
-
updated = true;
|
|
3593
|
-
return true;
|
|
3594
|
-
});
|
|
3595
|
-
return updated;
|
|
3596
|
-
}
|
|
3597
|
-
|
|
3598
4368
|
// src/config/import.ts
|
|
3599
|
-
function
|
|
4369
|
+
function isRecord5(value) {
|
|
3600
4370
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3601
4371
|
}
|
|
3602
4372
|
function isStringRecord2(value) {
|
|
3603
|
-
return
|
|
4373
|
+
return isRecord5(value) && Object.values(value).every((v) => typeof v === "string");
|
|
3604
4374
|
}
|
|
3605
4375
|
function tryParse(text) {
|
|
3606
4376
|
try {
|
|
@@ -3630,17 +4400,17 @@ function importClaude(text, names) {
|
|
|
3630
4400
|
result.errors.push(error);
|
|
3631
4401
|
return result;
|
|
3632
4402
|
}
|
|
3633
|
-
if (!
|
|
4403
|
+
if (!isRecord5(parsed)) {
|
|
3634
4404
|
result.errors.push("expected an object");
|
|
3635
4405
|
return result;
|
|
3636
4406
|
}
|
|
3637
4407
|
const mcpServers = parsed.mcpServers;
|
|
3638
|
-
if (!
|
|
4408
|
+
if (!isRecord5(mcpServers)) {
|
|
3639
4409
|
result.errors.push('no "mcpServers" object');
|
|
3640
4410
|
return result;
|
|
3641
4411
|
}
|
|
3642
4412
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
3643
|
-
if (!
|
|
4413
|
+
if (!isRecord5(entry)) {
|
|
3644
4414
|
result.errors.push(`${name}: not an object`);
|
|
3645
4415
|
continue;
|
|
3646
4416
|
}
|
|
@@ -3655,17 +4425,17 @@ function importVSCode(text, names) {
|
|
|
3655
4425
|
result.errors.push(error);
|
|
3656
4426
|
return result;
|
|
3657
4427
|
}
|
|
3658
|
-
if (!
|
|
4428
|
+
if (!isRecord5(parsed)) {
|
|
3659
4429
|
result.errors.push("expected an object");
|
|
3660
4430
|
return result;
|
|
3661
4431
|
}
|
|
3662
4432
|
const servers = parsed.servers ?? parsed.mcpServers;
|
|
3663
|
-
if (!
|
|
4433
|
+
if (!isRecord5(servers)) {
|
|
3664
4434
|
result.errors.push('no "servers" or "mcpServers" object');
|
|
3665
4435
|
return result;
|
|
3666
4436
|
}
|
|
3667
4437
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
3668
|
-
if (!
|
|
4438
|
+
if (!isRecord5(entry)) {
|
|
3669
4439
|
result.errors.push(`${name}: not an object`);
|
|
3670
4440
|
continue;
|
|
3671
4441
|
}
|
|
@@ -3699,17 +4469,17 @@ function importSixServers(text, names) {
|
|
|
3699
4469
|
result.errors.push(error);
|
|
3700
4470
|
return result;
|
|
3701
4471
|
}
|
|
3702
|
-
if (!
|
|
4472
|
+
if (!isRecord5(parsed)) {
|
|
3703
4473
|
result.errors.push("expected an object");
|
|
3704
4474
|
return result;
|
|
3705
4475
|
}
|
|
3706
4476
|
const servers = parsed.servers;
|
|
3707
|
-
if (!
|
|
4477
|
+
if (!isRecord5(servers)) {
|
|
3708
4478
|
result.errors.push('no "servers" object');
|
|
3709
4479
|
return result;
|
|
3710
4480
|
}
|
|
3711
4481
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
3712
|
-
if (!
|
|
4482
|
+
if (!isRecord5(entry)) {
|
|
3713
4483
|
result.errors.push(`${name}: not an object`);
|
|
3714
4484
|
continue;
|
|
3715
4485
|
}
|
|
@@ -3717,7 +4487,7 @@ function importSixServers(text, names) {
|
|
|
3717
4487
|
if (typeof entry.url === "string") config.url = entry.url;
|
|
3718
4488
|
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
3719
4489
|
if (typeof entry.exposure === "string") config.exposure = entry.exposure;
|
|
3720
|
-
if (
|
|
4490
|
+
if (isRecord5(entry.toolExposure)) config.toolExposure = entry.toolExposure;
|
|
3721
4491
|
result.servers.push({ name, config });
|
|
3722
4492
|
}
|
|
3723
4493
|
return result;
|
|
@@ -3734,7 +4504,7 @@ function importCodexToml(text, names) {
|
|
|
3734
4504
|
if (fields.command) config.command = fields.command;
|
|
3735
4505
|
if (fields.args) config.args = fields.args;
|
|
3736
4506
|
if (fields.url) config.url = fields.url;
|
|
3737
|
-
if (fields.env &&
|
|
4507
|
+
if (fields.env && isRecord5(fields.env)) config.env = fields.env;
|
|
3738
4508
|
result.servers.push({ name, config });
|
|
3739
4509
|
}
|
|
3740
4510
|
return result;
|
|
@@ -3786,7 +4556,7 @@ function convertOpenCodeLocal(entry) {
|
|
|
3786
4556
|
} else if (typeof entry.command === "string") {
|
|
3787
4557
|
config.command = entry.command;
|
|
3788
4558
|
}
|
|
3789
|
-
if (
|
|
4559
|
+
if (isRecord5(entry.environment)) {
|
|
3790
4560
|
const env = {};
|
|
3791
4561
|
for (const [k, v] of Object.entries(entry.environment)) {
|
|
3792
4562
|
if (typeof v === "string")
|
|
@@ -3809,17 +4579,17 @@ function importOpenCode(text, names) {
|
|
|
3809
4579
|
result.errors.push(error);
|
|
3810
4580
|
return result;
|
|
3811
4581
|
}
|
|
3812
|
-
if (!
|
|
4582
|
+
if (!isRecord5(parsed)) {
|
|
3813
4583
|
result.errors.push("expected an object");
|
|
3814
4584
|
return result;
|
|
3815
4585
|
}
|
|
3816
4586
|
const mcpServers = parsed.mcpServers ?? parsed.mcp;
|
|
3817
|
-
if (!
|
|
4587
|
+
if (!isRecord5(mcpServers)) {
|
|
3818
4588
|
result.errors.push("no mcp servers found");
|
|
3819
4589
|
return result;
|
|
3820
4590
|
}
|
|
3821
4591
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
3822
|
-
if (!
|
|
4592
|
+
if (!isRecord5(entry)) {
|
|
3823
4593
|
result.errors.push(`${name}: not an object`);
|
|
3824
4594
|
continue;
|
|
3825
4595
|
}
|
|
@@ -3835,9 +4605,9 @@ function detectFormat(text, filename) {
|
|
|
3835
4605
|
if (filename.includes(".vscode")) return "vscode";
|
|
3836
4606
|
try {
|
|
3837
4607
|
const parsed = JSON.parse(text);
|
|
3838
|
-
if (
|
|
3839
|
-
if (parsed.servers &&
|
|
3840
|
-
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";
|
|
3841
4611
|
if (parsed.mcpServers) return "claude";
|
|
3842
4612
|
if (parsed.mcp) return "opencode";
|
|
3843
4613
|
}
|
|
@@ -4406,9 +5176,7 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
4406
5176
|
"task",
|
|
4407
5177
|
"login",
|
|
4408
5178
|
"logout",
|
|
4409
|
-
"code"
|
|
4410
|
-
"discover",
|
|
4411
|
-
"inbox"
|
|
5179
|
+
"code"
|
|
4412
5180
|
]);
|
|
4413
5181
|
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe", "serve"]);
|
|
4414
5182
|
function helpText(version) {
|
|
@@ -4436,8 +5204,8 @@ Configuration verbs:
|
|
|
4436
5204
|
Server and code verbs:
|
|
4437
5205
|
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
4438
5206
|
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
4439
|
-
discover <url> Find MCP servers
|
|
4440
|
-
inbox
|
|
5207
|
+
discover <url> Find MCP servers at a URL
|
|
5208
|
+
inbox [--json] List discovery candidates
|
|
4441
5209
|
help [verb] Show help
|
|
4442
5210
|
|
|
4443
5211
|
Options:
|
|
@@ -4752,6 +5520,37 @@ async function dispatchConfigVerb(platform, verb, args) {
|
|
|
4752
5520
|
return 1;
|
|
4753
5521
|
}
|
|
4754
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
|
+
}
|
|
4755
5554
|
async function run(platform, version, args) {
|
|
4756
5555
|
if (args.includes("--version") || args.includes("-V")) {
|
|
4757
5556
|
await platform.write(1, `${version}
|
|
@@ -4772,21 +5571,19 @@ async function run(platform, version, args) {
|
|
|
4772
5571
|
return 2;
|
|
4773
5572
|
}
|
|
4774
5573
|
if (parsed.verb in CONFIG_VERBS) {
|
|
5574
|
+
const disc = await dispatchDiscoveryVerb(platform, parsed, args);
|
|
5575
|
+
if (disc !== void 0) return disc;
|
|
4775
5576
|
return dispatchConfigVerb(platform, parsed.verb, args);
|
|
4776
5577
|
}
|
|
5578
|
+
const discResult = await dispatchDiscoveryVerb(platform, parsed, args);
|
|
5579
|
+
if (discResult !== void 0) return discResult;
|
|
4777
5580
|
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
4778
5581
|
await platform.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
4779
5582
|
`);
|
|
4780
5583
|
return 1;
|
|
4781
5584
|
}
|
|
4782
5585
|
if (parsed.verb === "serve") {
|
|
4783
|
-
|
|
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);
|
|
5586
|
+
return dispatchServeVerb(platform, args, version);
|
|
4790
5587
|
}
|
|
4791
5588
|
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
4792
5589
|
await platform.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
@@ -4959,7 +5756,7 @@ function createKernelPlatform(ctx) {
|
|
|
4959
5756
|
async function main(ctx) {
|
|
4960
5757
|
const platform = createKernelPlatform(ctx);
|
|
4961
5758
|
const args = ctx.argv.slice(1);
|
|
4962
|
-
return run(platform, "0.
|
|
5759
|
+
return run(platform, "0.6.0", args);
|
|
4963
5760
|
}
|
|
4964
5761
|
export {
|
|
4965
5762
|
main as default
|