@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.
@@ -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 nextId = 1;
1478
+ let nextId2 = 1;
1479
1479
  let negotiatedVersion;
1480
1480
  async function request(method, params) {
1481
- const id = nextId++;
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 isRecord3(value) {
4369
+ function isRecord5(value) {
3600
4370
  return typeof value === "object" && value !== null && !Array.isArray(value);
3601
4371
  }
3602
4372
  function isStringRecord2(value) {
3603
- return isRecord3(value) && Object.values(value).every((v) => typeof v === "string");
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 (!isRecord3(parsed)) {
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 (!isRecord3(mcpServers)) {
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 (!isRecord3(entry)) {
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 (!isRecord3(parsed)) {
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 (!isRecord3(servers)) {
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 (!isRecord3(entry)) {
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 (!isRecord3(parsed)) {
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 (!isRecord3(servers)) {
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 (!isRecord3(entry)) {
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 (isRecord3(entry.toolExposure)) config.toolExposure = entry.toolExposure;
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 && isRecord3(fields.env)) config.env = 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 (isRecord3(entry.environment)) {
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 (!isRecord3(parsed)) {
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 (!isRecord3(mcpServers)) {
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 (!isRecord3(entry)) {
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 (isRecord3(parsed)) {
3839
- if (parsed.servers && isRecord3(parsed.servers) && parsed.version !== void 0) return "six";
3840
- if (parsed.servers && isRecord3(parsed.servers)) return "vscode";
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 (not implemented yet)
4440
- inbox Discovery inbox (not implemented yet)
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
- const serveArgs = args.slice(args.indexOf("serve") + 1);
4784
- const serveOptions = parseServeFlags(serveArgs);
4785
- if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
4786
- await platform.write(2, "mcpcat serve: no commands or --tool templates given\n");
4787
- return 2;
4788
- }
4789
- return runServe(platform, serveOptions, version);
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.5.0", args);
5759
+ return run(platform, "0.6.0", args);
4963
5760
  }
4964
5761
  export {
4965
5762
  main as default