@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/host/mcpcat.mjs
CHANGED
|
@@ -2008,10 +2008,10 @@ async function createStatelessConnection(options) {
|
|
|
2008
2008
|
timeoutMs = 6e4,
|
|
2009
2009
|
clientInfo = { name: "mcpcat", version: "0.0.0" }
|
|
2010
2010
|
} = options;
|
|
2011
|
-
let
|
|
2011
|
+
let nextId2 = 1;
|
|
2012
2012
|
let negotiatedVersion;
|
|
2013
2013
|
async function request(method, params) {
|
|
2014
|
-
const id =
|
|
2014
|
+
const id = nextId2++;
|
|
2015
2015
|
const headers = {
|
|
2016
2016
|
"Content-Type": "application/json",
|
|
2017
2017
|
Accept: "application/json, text/event-stream",
|
|
@@ -2549,6 +2549,13 @@ function userConfigPath(platform2) {
|
|
|
2549
2549
|
function projectConfigPath(platform2) {
|
|
2550
2550
|
return `${platform2.cwd}/.pi/mcp.json`;
|
|
2551
2551
|
}
|
|
2552
|
+
function stateDir(platform2) {
|
|
2553
|
+
if (platform2.env.XDG_STATE_HOME) return `${platform2.env.XDG_STATE_HOME}/mcpcat`;
|
|
2554
|
+
const home = platform2.env.HOME ?? platform2.env.USERPROFILE ?? "/";
|
|
2555
|
+
if (platform2.env.LOCALAPPDATA) return `${platform2.env.LOCALAPPDATA}\\mcpcat\\state`;
|
|
2556
|
+
if (platform2.env.SLICC_KERNEL) return `${home}/.local/state/mcpcat`;
|
|
2557
|
+
return `${home}/Library/Application Support/mcpcat`;
|
|
2558
|
+
}
|
|
2552
2559
|
|
|
2553
2560
|
// src/config/index.ts
|
|
2554
2561
|
var MergedConfigSource = class _MergedConfigSource {
|
|
@@ -2589,6 +2596,915 @@ var MergedConfigSource = class _MergedConfigSource {
|
|
|
2589
2596
|
}
|
|
2590
2597
|
};
|
|
2591
2598
|
|
|
2599
|
+
// src/config/lock.ts
|
|
2600
|
+
var LOCK_STALE_MS = 1e4;
|
|
2601
|
+
var LOCK_POLL_MS = 50;
|
|
2602
|
+
var LOCK_MAX_WAIT_MS = 5e3;
|
|
2603
|
+
function isEexist(err) {
|
|
2604
|
+
if (err instanceof Error && "code" in err)
|
|
2605
|
+
return err.code === "EEXIST";
|
|
2606
|
+
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
2607
|
+
return false;
|
|
2608
|
+
}
|
|
2609
|
+
async function removeStaleLock(fs, lockPath) {
|
|
2610
|
+
try {
|
|
2611
|
+
const s = await fs.stat(lockPath);
|
|
2612
|
+
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
2613
|
+
try {
|
|
2614
|
+
await fs.rmdir(lockPath);
|
|
2615
|
+
} catch {
|
|
2616
|
+
}
|
|
2617
|
+
return true;
|
|
2618
|
+
}
|
|
2619
|
+
} catch {
|
|
2620
|
+
}
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
async function acquireLock(fs, configPath) {
|
|
2624
|
+
const lockPath = `${configPath}.lock`;
|
|
2625
|
+
const start = Date.now();
|
|
2626
|
+
while (true) {
|
|
2627
|
+
try {
|
|
2628
|
+
await fs.mkdirExclusive(lockPath);
|
|
2629
|
+
break;
|
|
2630
|
+
} catch (err) {
|
|
2631
|
+
if (!isEexist(err)) throw err;
|
|
2632
|
+
await removeStaleLock(fs, lockPath);
|
|
2633
|
+
}
|
|
2634
|
+
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
2635
|
+
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
2636
|
+
}
|
|
2637
|
+
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
2638
|
+
}
|
|
2639
|
+
let released = false;
|
|
2640
|
+
return async () => {
|
|
2641
|
+
if (released) return;
|
|
2642
|
+
released = true;
|
|
2643
|
+
try {
|
|
2644
|
+
await fs.rmdir(lockPath);
|
|
2645
|
+
} catch {
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/config/edit.ts
|
|
2651
|
+
function isRecord2(value) {
|
|
2652
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2653
|
+
}
|
|
2654
|
+
function detectIndent(text) {
|
|
2655
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
2656
|
+
return match ? match[1] : " ";
|
|
2657
|
+
}
|
|
2658
|
+
async function readText(fs, path) {
|
|
2659
|
+
try {
|
|
2660
|
+
const data = await fs.readFile(path);
|
|
2661
|
+
return new TextDecoder().decode(data);
|
|
2662
|
+
} catch {
|
|
2663
|
+
return void 0;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async function editMcpServers(fs, path, edit) {
|
|
2667
|
+
const release = await acquireLock(fs, path);
|
|
2668
|
+
try {
|
|
2669
|
+
const text = await readText(fs, path);
|
|
2670
|
+
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
2671
|
+
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
2672
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
2673
|
+
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
2674
|
+
if (!edit(servers, parsed)) return { changed: false };
|
|
2675
|
+
const indent = text ? detectIndent(text) : " ";
|
|
2676
|
+
const output = `${JSON.stringify(parsed, null, indent)}
|
|
2677
|
+
`;
|
|
2678
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
2679
|
+
if (dir) {
|
|
2680
|
+
try {
|
|
2681
|
+
await fs.mkdir(dir);
|
|
2682
|
+
} catch {
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
2686
|
+
await fs.writeFile(tmpPath, output);
|
|
2687
|
+
await fs.rename(tmpPath, path);
|
|
2688
|
+
return { changed: true };
|
|
2689
|
+
} finally {
|
|
2690
|
+
await release();
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
async function addServer(fs, path, name, config) {
|
|
2694
|
+
let replaced = false;
|
|
2695
|
+
await editMcpServers(fs, path, (servers, parsed) => {
|
|
2696
|
+
const target = servers ?? {};
|
|
2697
|
+
replaced = target[name] !== void 0;
|
|
2698
|
+
target[name] = config;
|
|
2699
|
+
parsed.mcpServers = target;
|
|
2700
|
+
return true;
|
|
2701
|
+
});
|
|
2702
|
+
return { replaced };
|
|
2703
|
+
}
|
|
2704
|
+
async function removeServer(fs, path, name) {
|
|
2705
|
+
let removed = false;
|
|
2706
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2707
|
+
if (!servers || servers[name] === void 0) return false;
|
|
2708
|
+
delete servers[name];
|
|
2709
|
+
removed = true;
|
|
2710
|
+
return true;
|
|
2711
|
+
});
|
|
2712
|
+
return removed;
|
|
2713
|
+
}
|
|
2714
|
+
async function updateServer(fs, path, name, patch) {
|
|
2715
|
+
let updated = false;
|
|
2716
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2717
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2718
|
+
const entry = servers[name];
|
|
2719
|
+
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
2720
|
+
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
2721
|
+
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
2722
|
+
updated = true;
|
|
2723
|
+
return true;
|
|
2724
|
+
});
|
|
2725
|
+
return updated;
|
|
2726
|
+
}
|
|
2727
|
+
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
2728
|
+
let updated = false;
|
|
2729
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2730
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2731
|
+
const entry = servers[name];
|
|
2732
|
+
if (toolGlob) {
|
|
2733
|
+
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
2734
|
+
te[toolGlob] = exposure;
|
|
2735
|
+
entry.toolExposure = te;
|
|
2736
|
+
} else {
|
|
2737
|
+
entry.exposure = exposure;
|
|
2738
|
+
}
|
|
2739
|
+
updated = true;
|
|
2740
|
+
return true;
|
|
2741
|
+
});
|
|
2742
|
+
return updated;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// src/discovery/extract.ts
|
|
2746
|
+
var nextId = 1;
|
|
2747
|
+
function candidateId() {
|
|
2748
|
+
return String(nextId++);
|
|
2749
|
+
}
|
|
2750
|
+
function resetIdCounter(start = 1) {
|
|
2751
|
+
nextId = start;
|
|
2752
|
+
}
|
|
2753
|
+
function isRecord3(v) {
|
|
2754
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2755
|
+
}
|
|
2756
|
+
function checkUrnPublisher(identifier, originHost) {
|
|
2757
|
+
const urnMatch = /^urn:air:([^:]+):/.exec(identifier);
|
|
2758
|
+
if (!urnMatch) return void 0;
|
|
2759
|
+
const urnDomain = urnMatch[1];
|
|
2760
|
+
if (domainMatch(urnDomain, originHost)) {
|
|
2761
|
+
return { type: "urn-publisher", detail: `${urnDomain} matches ${originHost}` };
|
|
2762
|
+
}
|
|
2763
|
+
return { type: "urn-publisher-mismatch", detail: `${urnDomain} does not match ${originHost}` };
|
|
2764
|
+
}
|
|
2765
|
+
function checkTrustManifest(entry, originHost) {
|
|
2766
|
+
if (!isRecord3(entry.trustManifest) || typeof entry.trustManifest.identity !== "string")
|
|
2767
|
+
return void 0;
|
|
2768
|
+
const identity = entry.trustManifest.identity;
|
|
2769
|
+
if (domainMatch(identity, originHost)) {
|
|
2770
|
+
return { type: "trust-manifest", detail: `identity ${identity} matches origin` };
|
|
2771
|
+
}
|
|
2772
|
+
return {
|
|
2773
|
+
type: "trust-manifest-mismatch",
|
|
2774
|
+
detail: `identity ${identity} does not match ${originHost}`
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
function extractArd(event) {
|
|
2778
|
+
const candidates = [];
|
|
2779
|
+
const summary = event.summary;
|
|
2780
|
+
if (!isRecord3(summary)) return candidates;
|
|
2781
|
+
const entries = summary.entries;
|
|
2782
|
+
if (!Array.isArray(entries)) return candidates;
|
|
2783
|
+
for (const entry of entries) {
|
|
2784
|
+
if (!isRecord3(entry)) continue;
|
|
2785
|
+
if (entry.type !== "application/mcp-server-card+json") continue;
|
|
2786
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
2787
|
+
if (!url) continue;
|
|
2788
|
+
const trust = [];
|
|
2789
|
+
const originHost = hostFromUrl(event.origin);
|
|
2790
|
+
const identifier = typeof entry.identifier === "string" ? entry.identifier : void 0;
|
|
2791
|
+
if (identifier) {
|
|
2792
|
+
const urnSignal = checkUrnPublisher(identifier, originHost);
|
|
2793
|
+
if (urnSignal) trust.push(urnSignal);
|
|
2794
|
+
}
|
|
2795
|
+
const tmSignal = checkTrustManifest(entry, originHost);
|
|
2796
|
+
if (tmSignal) trust.push(tmSignal);
|
|
2797
|
+
candidates.push({
|
|
2798
|
+
id: candidateId(),
|
|
2799
|
+
endpoint: url,
|
|
2800
|
+
standard: "ard",
|
|
2801
|
+
origin: event.origin,
|
|
2802
|
+
name: typeof entry.displayName === "string" ? entry.displayName : void 0,
|
|
2803
|
+
transport: "http",
|
|
2804
|
+
trust,
|
|
2805
|
+
status: event.status,
|
|
2806
|
+
reason: event.reason,
|
|
2807
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return candidates;
|
|
2811
|
+
}
|
|
2812
|
+
function extractAiCatalog(event) {
|
|
2813
|
+
const candidates = [];
|
|
2814
|
+
const summary = event.summary;
|
|
2815
|
+
if (!isRecord3(summary)) return candidates;
|
|
2816
|
+
const entries = summary.entries;
|
|
2817
|
+
if (!Array.isArray(entries)) return candidates;
|
|
2818
|
+
for (const entry of entries) {
|
|
2819
|
+
if (!isRecord3(entry)) continue;
|
|
2820
|
+
if (entry.type !== "application/mcp-server-card+json" && entry.type !== "mcp-server") continue;
|
|
2821
|
+
const url = typeof entry.url === "string" ? entry.url : void 0;
|
|
2822
|
+
if (!url) continue;
|
|
2823
|
+
candidates.push({
|
|
2824
|
+
id: candidateId(),
|
|
2825
|
+
endpoint: url,
|
|
2826
|
+
standard: "ai-catalog",
|
|
2827
|
+
origin: event.origin,
|
|
2828
|
+
name: typeof entry.displayName === "string" ? entry.displayName : void 0,
|
|
2829
|
+
transport: "http",
|
|
2830
|
+
trust: [],
|
|
2831
|
+
status: event.status,
|
|
2832
|
+
reason: event.reason,
|
|
2833
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
return candidates;
|
|
2837
|
+
}
|
|
2838
|
+
function extractUcp(event) {
|
|
2839
|
+
const candidates = [];
|
|
2840
|
+
const summary = event.summary;
|
|
2841
|
+
if (!isRecord3(summary)) return candidates;
|
|
2842
|
+
const services = summary.services;
|
|
2843
|
+
if (!isRecord3(services)) return candidates;
|
|
2844
|
+
for (const [svcName, svcValue] of Object.entries(services)) {
|
|
2845
|
+
if (!Array.isArray(svcValue)) continue;
|
|
2846
|
+
for (const transport of svcValue) {
|
|
2847
|
+
if (!isRecord3(transport)) continue;
|
|
2848
|
+
if (transport.transport !== "mcp") continue;
|
|
2849
|
+
const endpoint = typeof transport.endpoint === "string" ? transport.endpoint : void 0;
|
|
2850
|
+
if (!endpoint) continue;
|
|
2851
|
+
candidates.push({
|
|
2852
|
+
id: candidateId(),
|
|
2853
|
+
endpoint,
|
|
2854
|
+
standard: "ucp",
|
|
2855
|
+
origin: event.origin,
|
|
2856
|
+
name: svcName,
|
|
2857
|
+
transport: "http",
|
|
2858
|
+
trust: [],
|
|
2859
|
+
status: event.status,
|
|
2860
|
+
reason: event.reason,
|
|
2861
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return candidates;
|
|
2866
|
+
}
|
|
2867
|
+
function extractPoppy(event) {
|
|
2868
|
+
const candidates = [];
|
|
2869
|
+
const summary = event.summary;
|
|
2870
|
+
if (!isRecord3(summary)) return candidates;
|
|
2871
|
+
const apis = summary.apis;
|
|
2872
|
+
if (!Array.isArray(apis)) return candidates;
|
|
2873
|
+
for (const api of apis) {
|
|
2874
|
+
if (!isRecord3(api)) continue;
|
|
2875
|
+
if (api.type !== "mcp") continue;
|
|
2876
|
+
const url = typeof api.url === "string" ? api.url : void 0;
|
|
2877
|
+
if (!url) continue;
|
|
2878
|
+
candidates.push({
|
|
2879
|
+
id: candidateId(),
|
|
2880
|
+
endpoint: url,
|
|
2881
|
+
standard: "poppy",
|
|
2882
|
+
origin: event.origin,
|
|
2883
|
+
transport: "http",
|
|
2884
|
+
trust: [],
|
|
2885
|
+
status: event.status,
|
|
2886
|
+
reason: event.reason,
|
|
2887
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
return candidates;
|
|
2891
|
+
}
|
|
2892
|
+
function extractNlweb(event) {
|
|
2893
|
+
const summary = event.summary;
|
|
2894
|
+
if (!isRecord3(summary)) return [];
|
|
2895
|
+
const mcpUrl = typeof summary.mcpUrl === "string" ? summary.mcpUrl : void 0;
|
|
2896
|
+
if (!mcpUrl) return [];
|
|
2897
|
+
return [
|
|
2898
|
+
{
|
|
2899
|
+
id: candidateId(),
|
|
2900
|
+
endpoint: mcpUrl,
|
|
2901
|
+
standard: "nlweb",
|
|
2902
|
+
origin: event.origin,
|
|
2903
|
+
transport: "http",
|
|
2904
|
+
trust: [],
|
|
2905
|
+
status: event.status,
|
|
2906
|
+
reason: event.reason,
|
|
2907
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2908
|
+
}
|
|
2909
|
+
];
|
|
2910
|
+
}
|
|
2911
|
+
function extractWebmcp(event) {
|
|
2912
|
+
return [
|
|
2913
|
+
{
|
|
2914
|
+
id: candidateId(),
|
|
2915
|
+
endpoint: event.url,
|
|
2916
|
+
standard: "webmcp",
|
|
2917
|
+
origin: event.origin,
|
|
2918
|
+
transport: "webmcp",
|
|
2919
|
+
trust: [],
|
|
2920
|
+
status: event.status,
|
|
2921
|
+
reason: event.reason,
|
|
2922
|
+
suggestion: "mcpcat serve --webmcp",
|
|
2923
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2924
|
+
}
|
|
2925
|
+
];
|
|
2926
|
+
}
|
|
2927
|
+
function hostFromUrl(url) {
|
|
2928
|
+
try {
|
|
2929
|
+
return new URL(url).hostname;
|
|
2930
|
+
} catch {
|
|
2931
|
+
return "";
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
var TWO_LABEL_TLDS = /* @__PURE__ */ new Set([
|
|
2935
|
+
"co.uk",
|
|
2936
|
+
"org.uk",
|
|
2937
|
+
"ac.uk",
|
|
2938
|
+
"co.jp",
|
|
2939
|
+
"or.jp",
|
|
2940
|
+
"co.kr",
|
|
2941
|
+
"co.nz",
|
|
2942
|
+
"co.za",
|
|
2943
|
+
"com.au",
|
|
2944
|
+
"com.br",
|
|
2945
|
+
"com.cn",
|
|
2946
|
+
"com.mx",
|
|
2947
|
+
"com.tw",
|
|
2948
|
+
"net.au",
|
|
2949
|
+
"org.au",
|
|
2950
|
+
"co.in",
|
|
2951
|
+
"com.sg",
|
|
2952
|
+
"co.il"
|
|
2953
|
+
]);
|
|
2954
|
+
function registrableDomain(host) {
|
|
2955
|
+
const h = host.toLowerCase().replace(/^www\./, "");
|
|
2956
|
+
const parts = h.split(".");
|
|
2957
|
+
if (parts.length <= 2) return h;
|
|
2958
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
2959
|
+
if (TWO_LABEL_TLDS.has(lastTwo) && parts.length > 2) {
|
|
2960
|
+
return parts.slice(-3).join(".");
|
|
2961
|
+
}
|
|
2962
|
+
return lastTwo;
|
|
2963
|
+
}
|
|
2964
|
+
function domainMatch(a, b) {
|
|
2965
|
+
return registrableDomain(a) === registrableDomain(b);
|
|
2966
|
+
}
|
|
2967
|
+
function extractMcp(event) {
|
|
2968
|
+
const summary = event.summary;
|
|
2969
|
+
if (!isRecord3(summary)) return [];
|
|
2970
|
+
const endpoint = typeof summary.endpoint === "string" ? summary.endpoint : void 0;
|
|
2971
|
+
if (!endpoint) return [];
|
|
2972
|
+
const trust = [];
|
|
2973
|
+
if (Array.isArray(summary.trust)) {
|
|
2974
|
+
for (const t of summary.trust) {
|
|
2975
|
+
if (isRecord3(t) && typeof t.type === "string" && typeof t.detail === "string") {
|
|
2976
|
+
trust.push({ type: t.type, detail: t.detail });
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const si = isRecord3(summary.serverInfo) ? summary.serverInfo : void 0;
|
|
2981
|
+
const name = si && typeof si.name === "string" ? si.name : void 0;
|
|
2982
|
+
return [
|
|
2983
|
+
{
|
|
2984
|
+
id: candidateId(),
|
|
2985
|
+
endpoint,
|
|
2986
|
+
standard: "mcp",
|
|
2987
|
+
origin: event.origin,
|
|
2988
|
+
name,
|
|
2989
|
+
transport: "http",
|
|
2990
|
+
trust,
|
|
2991
|
+
status: event.status,
|
|
2992
|
+
reason: event.reason,
|
|
2993
|
+
discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2994
|
+
}
|
|
2995
|
+
];
|
|
2996
|
+
}
|
|
2997
|
+
var EXTRACTORS = {
|
|
2998
|
+
ard: extractArd,
|
|
2999
|
+
"ai-catalog": extractAiCatalog,
|
|
3000
|
+
ucp: extractUcp,
|
|
3001
|
+
poppy: extractPoppy,
|
|
3002
|
+
nlweb: extractNlweb,
|
|
3003
|
+
webmcp: extractWebmcp,
|
|
3004
|
+
mcp: extractMcp
|
|
3005
|
+
};
|
|
3006
|
+
function extractCandidates(event) {
|
|
3007
|
+
const extractor = EXTRACTORS[event.standard];
|
|
3008
|
+
if (!extractor) return [];
|
|
3009
|
+
return extractor(event);
|
|
3010
|
+
}
|
|
3011
|
+
function deduplicateCandidates(candidates) {
|
|
3012
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3013
|
+
const result = [];
|
|
3014
|
+
for (const c of candidates) {
|
|
3015
|
+
const key = c.endpoint;
|
|
3016
|
+
if (seen.has(key)) continue;
|
|
3017
|
+
seen.add(key);
|
|
3018
|
+
result.push(c);
|
|
3019
|
+
}
|
|
3020
|
+
return result;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/discovery/inbox.ts
|
|
3024
|
+
function inboxPath(stateDir2) {
|
|
3025
|
+
return `${stateDir2}/inbox.json`;
|
|
3026
|
+
}
|
|
3027
|
+
async function readInbox(fs, stateDir2) {
|
|
3028
|
+
try {
|
|
3029
|
+
const data = await fs.readFile(inboxPath(stateDir2));
|
|
3030
|
+
const parsed = JSON.parse(new TextDecoder().decode(data));
|
|
3031
|
+
if (parsed && Array.isArray(parsed.candidates)) return parsed;
|
|
3032
|
+
} catch {
|
|
3033
|
+
}
|
|
3034
|
+
return { candidates: [] };
|
|
3035
|
+
}
|
|
3036
|
+
async function writeInbox(fs, stateDir2, inbox) {
|
|
3037
|
+
try {
|
|
3038
|
+
await fs.mkdir(stateDir2);
|
|
3039
|
+
} catch {
|
|
3040
|
+
}
|
|
3041
|
+
const json = JSON.stringify(inbox, null, 2) + "\n";
|
|
3042
|
+
await fs.writeFile(inboxPath(stateDir2), json);
|
|
3043
|
+
}
|
|
3044
|
+
async function appendCandidates(fs, stateDir2, candidates) {
|
|
3045
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
3046
|
+
const existing = new Set(inbox.candidates.map((c) => c.endpoint));
|
|
3047
|
+
for (const c of candidates) {
|
|
3048
|
+
if (existing.has(c.endpoint)) continue;
|
|
3049
|
+
existing.add(c.endpoint);
|
|
3050
|
+
inbox.candidates.push(c);
|
|
3051
|
+
}
|
|
3052
|
+
await writeInbox(fs, stateDir2, inbox);
|
|
3053
|
+
}
|
|
3054
|
+
async function listCandidates(fs, stateDir2) {
|
|
3055
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
3056
|
+
return inbox.candidates.slice().reverse();
|
|
3057
|
+
}
|
|
3058
|
+
async function getCandidate(fs, stateDir2, id) {
|
|
3059
|
+
const inbox = await readInbox(fs, stateDir2);
|
|
3060
|
+
return inbox.candidates.find((c) => c.id === id);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// src/discovery/probe.ts
|
|
3064
|
+
function isJsonContentType(ct) {
|
|
3065
|
+
if (!ct) return false;
|
|
3066
|
+
const lower = ct.toLowerCase();
|
|
3067
|
+
if (lower.includes("text/html")) return false;
|
|
3068
|
+
return lower.includes("json") || lower.includes("+json");
|
|
3069
|
+
}
|
|
3070
|
+
function parseLinkHeaders(headers) {
|
|
3071
|
+
const links = [];
|
|
3072
|
+
const raw = headers.get("link");
|
|
3073
|
+
if (!raw) return links;
|
|
3074
|
+
const parts = raw.split(/,(?=\s*<)/);
|
|
3075
|
+
for (const part of parts) {
|
|
3076
|
+
const urlMatch = /<([^>]+)>/.exec(part);
|
|
3077
|
+
const relMatch = /rel\s*=\s*"?([^";,\s]+)"?/i.exec(part);
|
|
3078
|
+
if (urlMatch && relMatch) {
|
|
3079
|
+
links.push({ url: urlMatch[1], rel: relMatch[1].toLowerCase() });
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
return links;
|
|
3083
|
+
}
|
|
3084
|
+
function wasRedirected(requestUrl, resp) {
|
|
3085
|
+
if (resp.redirected) return true;
|
|
3086
|
+
if (resp.url && resp.url !== requestUrl) return true;
|
|
3087
|
+
return false;
|
|
3088
|
+
}
|
|
3089
|
+
async function probeUrl(fetcher, url, accept) {
|
|
3090
|
+
let resp;
|
|
3091
|
+
try {
|
|
3092
|
+
resp = await fetcher(url, {
|
|
3093
|
+
redirect: "manual",
|
|
3094
|
+
headers: accept ? { Accept: accept } : void 0,
|
|
3095
|
+
credentials: "omit"
|
|
3096
|
+
});
|
|
3097
|
+
} catch {
|
|
3098
|
+
try {
|
|
3099
|
+
resp = await fetcher(url, {
|
|
3100
|
+
redirect: "follow",
|
|
3101
|
+
headers: accept ? { Accept: accept } : void 0,
|
|
3102
|
+
credentials: "omit"
|
|
3103
|
+
});
|
|
3104
|
+
if (wasRedirected(url, resp)) {
|
|
3105
|
+
return { ok: false, status: 301, headers: resp.headers, body: "" };
|
|
3106
|
+
}
|
|
3107
|
+
} catch {
|
|
3108
|
+
return void 0;
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
if (resp.status !== 200)
|
|
3112
|
+
return { ok: false, status: resp.status, headers: resp.headers, body: "" };
|
|
3113
|
+
const ct = resp.headers.get("content-type");
|
|
3114
|
+
if (!isJsonContentType(ct))
|
|
3115
|
+
return { ok: false, status: resp.status, headers: resp.headers, body: "" };
|
|
3116
|
+
const body = await resp.text();
|
|
3117
|
+
return { ok: true, status: 200, headers: resp.headers, body };
|
|
3118
|
+
}
|
|
3119
|
+
function isRecord4(v) {
|
|
3120
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
3121
|
+
}
|
|
3122
|
+
function makeEvent(standard, origin, url, via, page, status, summary, reason) {
|
|
3123
|
+
return {
|
|
3124
|
+
v: 1,
|
|
3125
|
+
standard,
|
|
3126
|
+
origin,
|
|
3127
|
+
url,
|
|
3128
|
+
via,
|
|
3129
|
+
page,
|
|
3130
|
+
targetId: "",
|
|
3131
|
+
status,
|
|
3132
|
+
summary,
|
|
3133
|
+
...reason ? { reason } : {}
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
function tryParseJson(body) {
|
|
3137
|
+
try {
|
|
3138
|
+
return JSON.parse(body);
|
|
3139
|
+
} catch {
|
|
3140
|
+
return void 0;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
function classifyArd(origin, url, via, page, body) {
|
|
3144
|
+
const parsed = tryParseJson(body);
|
|
3145
|
+
if (!isRecord4(parsed))
|
|
3146
|
+
return makeEvent("ard", origin, url, via, page, "invalid", null, "invalid JSON");
|
|
3147
|
+
if (!Array.isArray(parsed.entries))
|
|
3148
|
+
return makeEvent("ard", origin, url, via, page, "invalid", null, "missing entries[]");
|
|
3149
|
+
const entries = [];
|
|
3150
|
+
for (const entry of parsed.entries) {
|
|
3151
|
+
if (!isRecord4(entry)) continue;
|
|
3152
|
+
entries.push({
|
|
3153
|
+
identifier: entry.identifier,
|
|
3154
|
+
displayName: entry.displayName,
|
|
3155
|
+
type: entry.type,
|
|
3156
|
+
url: entry.url,
|
|
3157
|
+
capabilities: entry.capabilities,
|
|
3158
|
+
representativeQueries: entry.representativeQueries,
|
|
3159
|
+
trustManifest: entry.trustManifest
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
return makeEvent("ard", origin, url, via, page, "valid", { entries });
|
|
3163
|
+
}
|
|
3164
|
+
function classifyAiCatalog(origin, url, via, page, body) {
|
|
3165
|
+
const parsed = tryParseJson(body);
|
|
3166
|
+
if (!isRecord4(parsed))
|
|
3167
|
+
return makeEvent("ai-catalog", origin, url, via, page, "invalid", null, "invalid JSON");
|
|
3168
|
+
const entries = [];
|
|
3169
|
+
const items = Array.isArray(parsed.entries) ? parsed.entries : Array.isArray(parsed.items) ? parsed.items : void 0;
|
|
3170
|
+
if (!items)
|
|
3171
|
+
return makeEvent("ai-catalog", origin, url, via, page, "invalid", null, "no entries or items");
|
|
3172
|
+
for (const entry of items) {
|
|
3173
|
+
if (!isRecord4(entry)) continue;
|
|
3174
|
+
entries.push({
|
|
3175
|
+
displayName: entry.displayName ?? entry.name,
|
|
3176
|
+
type: entry.type,
|
|
3177
|
+
url: entry.url
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
return makeEvent("ai-catalog", origin, url, via, page, "valid", { entries });
|
|
3181
|
+
}
|
|
3182
|
+
async function probeWellKnown(fetcher, origin, page) {
|
|
3183
|
+
const events = [];
|
|
3184
|
+
const ardUrl = `${origin}/.well-known/ard.json`;
|
|
3185
|
+
const ardResult = await probeUrl(fetcher, ardUrl, "application/json");
|
|
3186
|
+
if (ardResult?.ok) {
|
|
3187
|
+
const ev = classifyArd(origin, ardUrl, "well-known", page, ardResult.body);
|
|
3188
|
+
if (ev) events.push(ev);
|
|
3189
|
+
}
|
|
3190
|
+
const aiCatUrl = `${origin}/.well-known/ai-catalog.json`;
|
|
3191
|
+
const aiCatResult = await probeUrl(fetcher, aiCatUrl, "application/json");
|
|
3192
|
+
if (aiCatResult?.ok) {
|
|
3193
|
+
const ev = classifyAiCatalog(origin, aiCatUrl, "well-known", page, aiCatResult.body);
|
|
3194
|
+
if (ev) events.push(ev);
|
|
3195
|
+
}
|
|
3196
|
+
return events;
|
|
3197
|
+
}
|
|
3198
|
+
async function probeLinkHeaders(fetcher, targetUrl, origin, page) {
|
|
3199
|
+
const events = [];
|
|
3200
|
+
const targetResult = await probeUrl(fetcher, targetUrl);
|
|
3201
|
+
if (!targetResult) return events;
|
|
3202
|
+
const links = parseLinkHeaders(targetResult.headers);
|
|
3203
|
+
for (const link of links) {
|
|
3204
|
+
const resolved = new URL(link.url, targetUrl).href;
|
|
3205
|
+
const classifier = link.rel === "ard" ? classifyArd : link.rel === "ai-catalog" ? classifyAiCatalog : null;
|
|
3206
|
+
if (!classifier) continue;
|
|
3207
|
+
const linkResult = await probeUrl(fetcher, resolved, "application/json");
|
|
3208
|
+
if (linkResult?.ok) {
|
|
3209
|
+
const ev = classifier(origin, resolved, "link-header", page, linkResult.body);
|
|
3210
|
+
if (ev) events.push(ev);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
return events;
|
|
3214
|
+
}
|
|
3215
|
+
function mcpJsonRpc(method, params, id = 1) {
|
|
3216
|
+
return JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
3217
|
+
}
|
|
3218
|
+
function parseServerInfo(result) {
|
|
3219
|
+
const si = isRecord4(result.serverInfo) ? result.serverInfo : void 0;
|
|
3220
|
+
if (!si) return void 0;
|
|
3221
|
+
return {
|
|
3222
|
+
name: typeof si.name === "string" ? si.name : void 0,
|
|
3223
|
+
version: typeof si.version === "string" ? si.version : void 0
|
|
3224
|
+
};
|
|
3225
|
+
}
|
|
3226
|
+
function checkAuthResponse(resp) {
|
|
3227
|
+
if (resp.status !== 401) return void 0;
|
|
3228
|
+
const wwwAuth = resp.headers.get("www-authenticate");
|
|
3229
|
+
if (!wwwAuth) return void 0;
|
|
3230
|
+
return { type: "auth-required", detail: wwwAuth.split(",")[0].trim() };
|
|
3231
|
+
}
|
|
3232
|
+
async function tryMcpPost(fetcher, url, body, extraHeaders) {
|
|
3233
|
+
const headers = {
|
|
3234
|
+
"Content-Type": "application/json",
|
|
3235
|
+
Accept: "application/json",
|
|
3236
|
+
...extraHeaders
|
|
3237
|
+
};
|
|
3238
|
+
try {
|
|
3239
|
+
const resp = await fetcher(url, {
|
|
3240
|
+
method: "POST",
|
|
3241
|
+
headers,
|
|
3242
|
+
body,
|
|
3243
|
+
redirect: "manual",
|
|
3244
|
+
credentials: "omit"
|
|
3245
|
+
});
|
|
3246
|
+
const authSignal = checkAuthResponse(resp);
|
|
3247
|
+
if (authSignal) return { url, trust: [authSignal] };
|
|
3248
|
+
if (resp.status === 200) {
|
|
3249
|
+
const text = await resp.text();
|
|
3250
|
+
const parsed = tryParseJson(text);
|
|
3251
|
+
if (isRecord4(parsed) && isRecord4(parsed.result)) {
|
|
3252
|
+
return { url, serverInfo: parseServerInfo(parsed.result), trust: [] };
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
} catch {
|
|
3256
|
+
}
|
|
3257
|
+
return void 0;
|
|
3258
|
+
}
|
|
3259
|
+
async function probeMcpEndpoint(fetcher, url) {
|
|
3260
|
+
const discoverBody = mcpJsonRpc("server/discover", {
|
|
3261
|
+
protocolVersion: "2026-07-28",
|
|
3262
|
+
clientInfo: { name: "mcpcat", version: "0.0.0" }
|
|
3263
|
+
});
|
|
3264
|
+
const hit = await tryMcpPost(fetcher, url, discoverBody, {
|
|
3265
|
+
"Mcp-Method": "server/discover"
|
|
3266
|
+
});
|
|
3267
|
+
if (hit) return hit;
|
|
3268
|
+
const initBody = mcpJsonRpc("initialize", {
|
|
3269
|
+
protocolVersion: "2025-11-25",
|
|
3270
|
+
clientInfo: { name: "mcpcat", version: "0.0.0" },
|
|
3271
|
+
capabilities: {}
|
|
3272
|
+
});
|
|
3273
|
+
return tryMcpPost(fetcher, url, initBody);
|
|
3274
|
+
}
|
|
3275
|
+
async function probeMcp(fetcher, origin, page, targetUrl) {
|
|
3276
|
+
const events = [];
|
|
3277
|
+
const urls = [targetUrl];
|
|
3278
|
+
const mcpUrl = `${origin}/mcp`;
|
|
3279
|
+
if (mcpUrl !== targetUrl) urls.push(mcpUrl);
|
|
3280
|
+
for (const url of urls) {
|
|
3281
|
+
const hit = await probeMcpEndpoint(fetcher, url);
|
|
3282
|
+
if (hit) {
|
|
3283
|
+
const summary = { endpoint: hit.url };
|
|
3284
|
+
if (hit.serverInfo) summary.serverInfo = hit.serverInfo;
|
|
3285
|
+
if (hit.trust.length > 0) summary.trust = hit.trust;
|
|
3286
|
+
events.push(makeEvent("mcp", origin, hit.url, "well-known", page, "valid", summary));
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
return events;
|
|
3290
|
+
}
|
|
3291
|
+
async function probeDiscover(platform2, targetUrl) {
|
|
3292
|
+
let origin;
|
|
3293
|
+
try {
|
|
3294
|
+
const u = new URL(targetUrl);
|
|
3295
|
+
origin = u.origin;
|
|
3296
|
+
} catch {
|
|
3297
|
+
return { events: [] };
|
|
3298
|
+
}
|
|
3299
|
+
const page = targetUrl;
|
|
3300
|
+
const fetcher = platform2.fetch;
|
|
3301
|
+
const events = [
|
|
3302
|
+
...await probeWellKnown(fetcher, origin, page),
|
|
3303
|
+
...await probeLinkHeaders(fetcher, targetUrl, origin, page),
|
|
3304
|
+
...await probeMcp(fetcher, origin, page, targetUrl)
|
|
3305
|
+
];
|
|
3306
|
+
return { events };
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
// src/discovery/verbs.ts
|
|
3310
|
+
async function readStdin(platform2) {
|
|
3311
|
+
const reader = platform2.stdin.getReader();
|
|
3312
|
+
const chunks = [];
|
|
3313
|
+
for (; ; ) {
|
|
3314
|
+
const { done, value } = await reader.read();
|
|
3315
|
+
if (done) break;
|
|
3316
|
+
chunks.push(value);
|
|
3317
|
+
}
|
|
3318
|
+
reader.releaseLock();
|
|
3319
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
3320
|
+
const merged = new Uint8Array(total);
|
|
3321
|
+
let offset = 0;
|
|
3322
|
+
for (const c of chunks) {
|
|
3323
|
+
merged.set(c, offset);
|
|
3324
|
+
offset += c.length;
|
|
3325
|
+
}
|
|
3326
|
+
return new TextDecoder().decode(merged);
|
|
3327
|
+
}
|
|
3328
|
+
function parseEvents(text) {
|
|
3329
|
+
const lines = text.trim().split("\n").filter((l) => l.trim().length > 0);
|
|
3330
|
+
const events = [];
|
|
3331
|
+
for (const line of lines) {
|
|
3332
|
+
let parsed;
|
|
3333
|
+
try {
|
|
3334
|
+
parsed = JSON.parse(line);
|
|
3335
|
+
} catch {
|
|
3336
|
+
return { error: `invalid JSON: ${line.slice(0, 80)}`, code: 2 };
|
|
3337
|
+
}
|
|
3338
|
+
const ev = parsed;
|
|
3339
|
+
if (typeof ev.v !== "number") return { error: "missing event version", code: 2 };
|
|
3340
|
+
if (ev.v !== 1) return { error: `unknown event version: ${ev.v}`, code: 2 };
|
|
3341
|
+
events.push(ev);
|
|
3342
|
+
}
|
|
3343
|
+
return events;
|
|
3344
|
+
}
|
|
3345
|
+
function formatCandidate(c) {
|
|
3346
|
+
const parts = [`#${c.id} ${c.standard} ${c.endpoint}`];
|
|
3347
|
+
if (c.name) parts[0] += ` (${c.name})`;
|
|
3348
|
+
if (c.status === "invalid") parts.push(` status: invalid \u2013 ${c.reason ?? "unknown"}`);
|
|
3349
|
+
if (c.suggestion) parts.push(` suggestion: ${c.suggestion}`);
|
|
3350
|
+
for (const t of c.trust) {
|
|
3351
|
+
const icon = t.type.includes("mismatch") ? "\u26A0" : "\u2713";
|
|
3352
|
+
parts.push(` ${icon} ${t.type}: ${t.detail}`);
|
|
3353
|
+
}
|
|
3354
|
+
return parts.join("\n");
|
|
3355
|
+
}
|
|
3356
|
+
function formatCandidateJson(c) {
|
|
3357
|
+
return {
|
|
3358
|
+
id: c.id,
|
|
3359
|
+
endpoint: c.endpoint,
|
|
3360
|
+
standard: c.standard,
|
|
3361
|
+
origin: c.origin,
|
|
3362
|
+
name: c.name,
|
|
3363
|
+
transport: c.transport,
|
|
3364
|
+
trust: c.trust,
|
|
3365
|
+
status: c.status,
|
|
3366
|
+
reason: c.reason,
|
|
3367
|
+
suggestion: c.suggestion,
|
|
3368
|
+
discoveredAt: c.discoveredAt
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
async function runFromDiscovery(platform2, args2) {
|
|
3372
|
+
let source = "-";
|
|
3373
|
+
for (const arg of args2) {
|
|
3374
|
+
if (arg !== "--from-discovery" && !arg.startsWith("-")) {
|
|
3375
|
+
source = arg;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
let text;
|
|
3379
|
+
if (source === "-") {
|
|
3380
|
+
text = await readStdin(platform2);
|
|
3381
|
+
} else {
|
|
3382
|
+
try {
|
|
3383
|
+
const data = await platform2.fs.readFile(source);
|
|
3384
|
+
text = new TextDecoder().decode(data);
|
|
3385
|
+
} catch {
|
|
3386
|
+
await platform2.write(2, `mcpcat add --from-discovery: cannot read '${source}'
|
|
3387
|
+
`);
|
|
3388
|
+
return 2;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
const result = parseEvents(text);
|
|
3392
|
+
if ("error" in result) {
|
|
3393
|
+
await platform2.write(2, `mcpcat add --from-discovery: ${result.error}
|
|
3394
|
+
`);
|
|
3395
|
+
return result.code;
|
|
3396
|
+
}
|
|
3397
|
+
resetIdCounter();
|
|
3398
|
+
let allCandidates = [];
|
|
3399
|
+
for (const event of result) {
|
|
3400
|
+
allCandidates.push(...extractCandidates(event));
|
|
3401
|
+
}
|
|
3402
|
+
allCandidates = deduplicateCandidates(allCandidates);
|
|
3403
|
+
if (allCandidates.length === 0) {
|
|
3404
|
+
await platform2.write(1, "no MCP candidates found\n");
|
|
3405
|
+
return 0;
|
|
3406
|
+
}
|
|
3407
|
+
const sd = stateDir(platform2);
|
|
3408
|
+
await appendCandidates(platform2.fs, sd, allCandidates);
|
|
3409
|
+
for (const c of allCandidates) {
|
|
3410
|
+
await platform2.write(1, `${formatCandidate(c)}
|
|
3411
|
+
`);
|
|
3412
|
+
}
|
|
3413
|
+
return 0;
|
|
3414
|
+
}
|
|
3415
|
+
async function runInbox(platform2, args2) {
|
|
3416
|
+
const json = args2.includes("--json");
|
|
3417
|
+
const sd = stateDir(platform2);
|
|
3418
|
+
const candidates = await listCandidates(platform2.fs, sd);
|
|
3419
|
+
if (candidates.length === 0) {
|
|
3420
|
+
await platform2.write(1, "inbox is empty\n");
|
|
3421
|
+
return 0;
|
|
3422
|
+
}
|
|
3423
|
+
if (json) {
|
|
3424
|
+
await platform2.write(1, JSON.stringify(candidates.map(formatCandidateJson), null, 2) + "\n");
|
|
3425
|
+
} else {
|
|
3426
|
+
for (const c of candidates) {
|
|
3427
|
+
await platform2.write(1, `${formatCandidate(c)}
|
|
3428
|
+
`);
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
return 0;
|
|
3432
|
+
}
|
|
3433
|
+
async function runAddCandidate(platform2, name, candidateId2, configFile) {
|
|
3434
|
+
const sd = stateDir(platform2);
|
|
3435
|
+
const candidate = await getCandidate(platform2.fs, sd, candidateId2);
|
|
3436
|
+
if (!candidate) {
|
|
3437
|
+
await platform2.write(2, `mcpcat add: candidate #${candidateId2} not found in inbox
|
|
3438
|
+
`);
|
|
3439
|
+
return 1;
|
|
3440
|
+
}
|
|
3441
|
+
if (candidate.transport === "webmcp") {
|
|
3442
|
+
await platform2.write(
|
|
3443
|
+
2,
|
|
3444
|
+
`mcpcat add: WebMCP candidates cannot be added directly; use: ${candidate.suggestion}
|
|
3445
|
+
`
|
|
3446
|
+
);
|
|
3447
|
+
return 2;
|
|
3448
|
+
}
|
|
3449
|
+
const path = configFile ?? userConfigPath(platform2);
|
|
3450
|
+
const config = { url: candidate.endpoint };
|
|
3451
|
+
const { replaced } = await addServer(platform2.fs, path, name, config);
|
|
3452
|
+
for (const t of candidate.trust) {
|
|
3453
|
+
if (t.type.includes("mismatch")) {
|
|
3454
|
+
await platform2.write(2, `mcpcat add: warning: ${t.type}: ${t.detail}
|
|
3455
|
+
`);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
await platform2.write(1, `${replaced ? "replaced" : "added"} ${name} in ${path}
|
|
3459
|
+
`);
|
|
3460
|
+
return 0;
|
|
3461
|
+
}
|
|
3462
|
+
async function runDiscover(platform2, args2) {
|
|
3463
|
+
const positional = [];
|
|
3464
|
+
let json = false;
|
|
3465
|
+
for (const arg of args2) {
|
|
3466
|
+
if (arg === "--json") {
|
|
3467
|
+
json = true;
|
|
3468
|
+
} else if (!arg.startsWith("-")) {
|
|
3469
|
+
positional.push(arg);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
if (positional.length < 1) {
|
|
3473
|
+
await platform2.write(2, "mcpcat discover: requires a URL\n");
|
|
3474
|
+
return 2;
|
|
3475
|
+
}
|
|
3476
|
+
const url = positional[0];
|
|
3477
|
+
if (!URL.canParse(url)) {
|
|
3478
|
+
await platform2.write(2, `mcpcat discover: '${url}' is not a valid URL
|
|
3479
|
+
`);
|
|
3480
|
+
return 2;
|
|
3481
|
+
}
|
|
3482
|
+
const { events } = await probeDiscover(platform2, url);
|
|
3483
|
+
resetIdCounter();
|
|
3484
|
+
let allCandidates = [];
|
|
3485
|
+
for (const event of events) {
|
|
3486
|
+
allCandidates.push(...extractCandidates(event));
|
|
3487
|
+
}
|
|
3488
|
+
allCandidates = deduplicateCandidates(allCandidates);
|
|
3489
|
+
if (allCandidates.length > 0) {
|
|
3490
|
+
const sd = stateDir(platform2);
|
|
3491
|
+
await appendCandidates(platform2.fs, sd, allCandidates);
|
|
3492
|
+
}
|
|
3493
|
+
if (allCandidates.length === 0) {
|
|
3494
|
+
await platform2.write(1, "no MCP candidates found\n");
|
|
3495
|
+
return 0;
|
|
3496
|
+
}
|
|
3497
|
+
if (json) {
|
|
3498
|
+
await platform2.write(1, JSON.stringify(allCandidates.map(formatCandidateJson), null, 2) + "\n");
|
|
3499
|
+
} else {
|
|
3500
|
+
for (const c of allCandidates) {
|
|
3501
|
+
await platform2.write(1, `${formatCandidate(c)}
|
|
3502
|
+
`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return 0;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
2592
3508
|
// src/config/expand.ts
|
|
2593
3509
|
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2594
3510
|
var PLACEHOLDER_TEST = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
@@ -3982,158 +4898,12 @@ async function handleMcpRequest(request, tools, call, serverName, version, allow
|
|
|
3982
4898
|
};
|
|
3983
4899
|
}
|
|
3984
4900
|
|
|
3985
|
-
// src/config/lock.ts
|
|
3986
|
-
var LOCK_STALE_MS = 1e4;
|
|
3987
|
-
var LOCK_POLL_MS = 50;
|
|
3988
|
-
var LOCK_MAX_WAIT_MS = 5e3;
|
|
3989
|
-
function isEexist(err) {
|
|
3990
|
-
if (err instanceof Error && "code" in err)
|
|
3991
|
-
return err.code === "EEXIST";
|
|
3992
|
-
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
3993
|
-
return false;
|
|
3994
|
-
}
|
|
3995
|
-
async function removeStaleLock(fs, lockPath) {
|
|
3996
|
-
try {
|
|
3997
|
-
const s = await fs.stat(lockPath);
|
|
3998
|
-
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
3999
|
-
try {
|
|
4000
|
-
await fs.rmdir(lockPath);
|
|
4001
|
-
} catch {
|
|
4002
|
-
}
|
|
4003
|
-
return true;
|
|
4004
|
-
}
|
|
4005
|
-
} catch {
|
|
4006
|
-
}
|
|
4007
|
-
return false;
|
|
4008
|
-
}
|
|
4009
|
-
async function acquireLock(fs, configPath) {
|
|
4010
|
-
const lockPath = `${configPath}.lock`;
|
|
4011
|
-
const start = Date.now();
|
|
4012
|
-
while (true) {
|
|
4013
|
-
try {
|
|
4014
|
-
await fs.mkdirExclusive(lockPath);
|
|
4015
|
-
break;
|
|
4016
|
-
} catch (err) {
|
|
4017
|
-
if (!isEexist(err)) throw err;
|
|
4018
|
-
await removeStaleLock(fs, lockPath);
|
|
4019
|
-
}
|
|
4020
|
-
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
4021
|
-
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
4022
|
-
}
|
|
4023
|
-
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
4024
|
-
}
|
|
4025
|
-
let released = false;
|
|
4026
|
-
return async () => {
|
|
4027
|
-
if (released) return;
|
|
4028
|
-
released = true;
|
|
4029
|
-
try {
|
|
4030
|
-
await fs.rmdir(lockPath);
|
|
4031
|
-
} catch {
|
|
4032
|
-
}
|
|
4033
|
-
};
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
// src/config/edit.ts
|
|
4037
|
-
function isRecord2(value) {
|
|
4038
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4039
|
-
}
|
|
4040
|
-
function detectIndent(text) {
|
|
4041
|
-
const match = /^([ \t]+)\S/m.exec(text);
|
|
4042
|
-
return match ? match[1] : " ";
|
|
4043
|
-
}
|
|
4044
|
-
async function readText(fs, path) {
|
|
4045
|
-
try {
|
|
4046
|
-
const data = await fs.readFile(path);
|
|
4047
|
-
return new TextDecoder().decode(data);
|
|
4048
|
-
} catch {
|
|
4049
|
-
return void 0;
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
async function editMcpServers(fs, path, edit) {
|
|
4053
|
-
const release = await acquireLock(fs, path);
|
|
4054
|
-
try {
|
|
4055
|
-
const text = await readText(fs, path);
|
|
4056
|
-
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
4057
|
-
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
4058
|
-
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
4059
|
-
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
4060
|
-
if (!edit(servers, parsed)) return { changed: false };
|
|
4061
|
-
const indent = text ? detectIndent(text) : " ";
|
|
4062
|
-
const output = `${JSON.stringify(parsed, null, indent)}
|
|
4063
|
-
`;
|
|
4064
|
-
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
4065
|
-
if (dir) {
|
|
4066
|
-
try {
|
|
4067
|
-
await fs.mkdir(dir);
|
|
4068
|
-
} catch {
|
|
4069
|
-
}
|
|
4070
|
-
}
|
|
4071
|
-
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
4072
|
-
await fs.writeFile(tmpPath, output);
|
|
4073
|
-
await fs.rename(tmpPath, path);
|
|
4074
|
-
return { changed: true };
|
|
4075
|
-
} finally {
|
|
4076
|
-
await release();
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
async function addServer(fs, path, name, config) {
|
|
4080
|
-
let replaced = false;
|
|
4081
|
-
await editMcpServers(fs, path, (servers, parsed) => {
|
|
4082
|
-
const target = servers ?? {};
|
|
4083
|
-
replaced = target[name] !== void 0;
|
|
4084
|
-
target[name] = config;
|
|
4085
|
-
parsed.mcpServers = target;
|
|
4086
|
-
return true;
|
|
4087
|
-
});
|
|
4088
|
-
return { replaced };
|
|
4089
|
-
}
|
|
4090
|
-
async function removeServer(fs, path, name) {
|
|
4091
|
-
let removed = false;
|
|
4092
|
-
await editMcpServers(fs, path, (servers) => {
|
|
4093
|
-
if (!servers || servers[name] === void 0) return false;
|
|
4094
|
-
delete servers[name];
|
|
4095
|
-
removed = true;
|
|
4096
|
-
return true;
|
|
4097
|
-
});
|
|
4098
|
-
return removed;
|
|
4099
|
-
}
|
|
4100
|
-
async function updateServer(fs, path, name, patch) {
|
|
4101
|
-
let updated = false;
|
|
4102
|
-
await editMcpServers(fs, path, (servers) => {
|
|
4103
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
4104
|
-
const entry = servers[name];
|
|
4105
|
-
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
4106
|
-
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
4107
|
-
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
4108
|
-
updated = true;
|
|
4109
|
-
return true;
|
|
4110
|
-
});
|
|
4111
|
-
return updated;
|
|
4112
|
-
}
|
|
4113
|
-
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
4114
|
-
let updated = false;
|
|
4115
|
-
await editMcpServers(fs, path, (servers) => {
|
|
4116
|
-
if (!servers || !isRecord2(servers[name])) return false;
|
|
4117
|
-
const entry = servers[name];
|
|
4118
|
-
if (toolGlob) {
|
|
4119
|
-
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
4120
|
-
te[toolGlob] = exposure;
|
|
4121
|
-
entry.toolExposure = te;
|
|
4122
|
-
} else {
|
|
4123
|
-
entry.exposure = exposure;
|
|
4124
|
-
}
|
|
4125
|
-
updated = true;
|
|
4126
|
-
return true;
|
|
4127
|
-
});
|
|
4128
|
-
return updated;
|
|
4129
|
-
}
|
|
4130
|
-
|
|
4131
4901
|
// src/config/import.ts
|
|
4132
|
-
function
|
|
4902
|
+
function isRecord5(value) {
|
|
4133
4903
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4134
4904
|
}
|
|
4135
4905
|
function isStringRecord2(value) {
|
|
4136
|
-
return
|
|
4906
|
+
return isRecord5(value) && Object.values(value).every((v) => typeof v === "string");
|
|
4137
4907
|
}
|
|
4138
4908
|
function tryParse(text) {
|
|
4139
4909
|
try {
|
|
@@ -4163,17 +4933,17 @@ function importClaude(text, names) {
|
|
|
4163
4933
|
result.errors.push(error);
|
|
4164
4934
|
return result;
|
|
4165
4935
|
}
|
|
4166
|
-
if (!
|
|
4936
|
+
if (!isRecord5(parsed)) {
|
|
4167
4937
|
result.errors.push("expected an object");
|
|
4168
4938
|
return result;
|
|
4169
4939
|
}
|
|
4170
4940
|
const mcpServers = parsed.mcpServers;
|
|
4171
|
-
if (!
|
|
4941
|
+
if (!isRecord5(mcpServers)) {
|
|
4172
4942
|
result.errors.push('no "mcpServers" object');
|
|
4173
4943
|
return result;
|
|
4174
4944
|
}
|
|
4175
4945
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
4176
|
-
if (!
|
|
4946
|
+
if (!isRecord5(entry)) {
|
|
4177
4947
|
result.errors.push(`${name}: not an object`);
|
|
4178
4948
|
continue;
|
|
4179
4949
|
}
|
|
@@ -4188,17 +4958,17 @@ function importVSCode(text, names) {
|
|
|
4188
4958
|
result.errors.push(error);
|
|
4189
4959
|
return result;
|
|
4190
4960
|
}
|
|
4191
|
-
if (!
|
|
4961
|
+
if (!isRecord5(parsed)) {
|
|
4192
4962
|
result.errors.push("expected an object");
|
|
4193
4963
|
return result;
|
|
4194
4964
|
}
|
|
4195
4965
|
const servers = parsed.servers ?? parsed.mcpServers;
|
|
4196
|
-
if (!
|
|
4966
|
+
if (!isRecord5(servers)) {
|
|
4197
4967
|
result.errors.push('no "servers" or "mcpServers" object');
|
|
4198
4968
|
return result;
|
|
4199
4969
|
}
|
|
4200
4970
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
4201
|
-
if (!
|
|
4971
|
+
if (!isRecord5(entry)) {
|
|
4202
4972
|
result.errors.push(`${name}: not an object`);
|
|
4203
4973
|
continue;
|
|
4204
4974
|
}
|
|
@@ -4232,17 +5002,17 @@ function importSixServers(text, names) {
|
|
|
4232
5002
|
result.errors.push(error);
|
|
4233
5003
|
return result;
|
|
4234
5004
|
}
|
|
4235
|
-
if (!
|
|
5005
|
+
if (!isRecord5(parsed)) {
|
|
4236
5006
|
result.errors.push("expected an object");
|
|
4237
5007
|
return result;
|
|
4238
5008
|
}
|
|
4239
5009
|
const servers = parsed.servers;
|
|
4240
|
-
if (!
|
|
5010
|
+
if (!isRecord5(servers)) {
|
|
4241
5011
|
result.errors.push('no "servers" object');
|
|
4242
5012
|
return result;
|
|
4243
5013
|
}
|
|
4244
5014
|
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
4245
|
-
if (!
|
|
5015
|
+
if (!isRecord5(entry)) {
|
|
4246
5016
|
result.errors.push(`${name}: not an object`);
|
|
4247
5017
|
continue;
|
|
4248
5018
|
}
|
|
@@ -4250,7 +5020,7 @@ function importSixServers(text, names) {
|
|
|
4250
5020
|
if (typeof entry.url === "string") config.url = entry.url;
|
|
4251
5021
|
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
4252
5022
|
if (typeof entry.exposure === "string") config.exposure = entry.exposure;
|
|
4253
|
-
if (
|
|
5023
|
+
if (isRecord5(entry.toolExposure)) config.toolExposure = entry.toolExposure;
|
|
4254
5024
|
result.servers.push({ name, config });
|
|
4255
5025
|
}
|
|
4256
5026
|
return result;
|
|
@@ -4267,7 +5037,7 @@ function importCodexToml(text, names) {
|
|
|
4267
5037
|
if (fields.command) config.command = fields.command;
|
|
4268
5038
|
if (fields.args) config.args = fields.args;
|
|
4269
5039
|
if (fields.url) config.url = fields.url;
|
|
4270
|
-
if (fields.env &&
|
|
5040
|
+
if (fields.env && isRecord5(fields.env)) config.env = fields.env;
|
|
4271
5041
|
result.servers.push({ name, config });
|
|
4272
5042
|
}
|
|
4273
5043
|
return result;
|
|
@@ -4319,7 +5089,7 @@ function convertOpenCodeLocal(entry) {
|
|
|
4319
5089
|
} else if (typeof entry.command === "string") {
|
|
4320
5090
|
config.command = entry.command;
|
|
4321
5091
|
}
|
|
4322
|
-
if (
|
|
5092
|
+
if (isRecord5(entry.environment)) {
|
|
4323
5093
|
const env = {};
|
|
4324
5094
|
for (const [k, v] of Object.entries(entry.environment)) {
|
|
4325
5095
|
if (typeof v === "string")
|
|
@@ -4342,17 +5112,17 @@ function importOpenCode(text, names) {
|
|
|
4342
5112
|
result.errors.push(error);
|
|
4343
5113
|
return result;
|
|
4344
5114
|
}
|
|
4345
|
-
if (!
|
|
5115
|
+
if (!isRecord5(parsed)) {
|
|
4346
5116
|
result.errors.push("expected an object");
|
|
4347
5117
|
return result;
|
|
4348
5118
|
}
|
|
4349
5119
|
const mcpServers = parsed.mcpServers ?? parsed.mcp;
|
|
4350
|
-
if (!
|
|
5120
|
+
if (!isRecord5(mcpServers)) {
|
|
4351
5121
|
result.errors.push("no mcp servers found");
|
|
4352
5122
|
return result;
|
|
4353
5123
|
}
|
|
4354
5124
|
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
4355
|
-
if (!
|
|
5125
|
+
if (!isRecord5(entry)) {
|
|
4356
5126
|
result.errors.push(`${name}: not an object`);
|
|
4357
5127
|
continue;
|
|
4358
5128
|
}
|
|
@@ -4368,9 +5138,9 @@ function detectFormat(text, filename) {
|
|
|
4368
5138
|
if (filename.includes(".vscode")) return "vscode";
|
|
4369
5139
|
try {
|
|
4370
5140
|
const parsed = JSON.parse(text);
|
|
4371
|
-
if (
|
|
4372
|
-
if (parsed.servers &&
|
|
4373
|
-
if (parsed.servers &&
|
|
5141
|
+
if (isRecord5(parsed)) {
|
|
5142
|
+
if (parsed.servers && isRecord5(parsed.servers) && parsed.version !== void 0) return "six";
|
|
5143
|
+
if (parsed.servers && isRecord5(parsed.servers)) return "vscode";
|
|
4374
5144
|
if (parsed.mcpServers) return "claude";
|
|
4375
5145
|
if (parsed.mcp) return "opencode";
|
|
4376
5146
|
}
|
|
@@ -4939,9 +5709,7 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
4939
5709
|
"task",
|
|
4940
5710
|
"login",
|
|
4941
5711
|
"logout",
|
|
4942
|
-
"code"
|
|
4943
|
-
"discover",
|
|
4944
|
-
"inbox"
|
|
5712
|
+
"code"
|
|
4945
5713
|
]);
|
|
4946
5714
|
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe", "serve"]);
|
|
4947
5715
|
function helpText(version) {
|
|
@@ -4969,8 +5737,8 @@ Configuration verbs:
|
|
|
4969
5737
|
Server and code verbs:
|
|
4970
5738
|
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
4971
5739
|
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
4972
|
-
discover <url> Find MCP servers
|
|
4973
|
-
inbox
|
|
5740
|
+
discover <url> Find MCP servers at a URL
|
|
5741
|
+
inbox [--json] List discovery candidates
|
|
4974
5742
|
help [verb] Show help
|
|
4975
5743
|
|
|
4976
5744
|
Options:
|
|
@@ -5285,6 +6053,37 @@ async function dispatchConfigVerb(platform2, verb, args2) {
|
|
|
5285
6053
|
return 1;
|
|
5286
6054
|
}
|
|
5287
6055
|
}
|
|
6056
|
+
async function dispatchDiscoveryVerb(platform2, parsed, args2) {
|
|
6057
|
+
if (parsed.verb === "add" && args2.includes("--from-discovery")) {
|
|
6058
|
+
return runFromDiscovery(platform2, args2.slice(1));
|
|
6059
|
+
}
|
|
6060
|
+
if (parsed.verb === "add" && args2.includes("--candidate")) {
|
|
6061
|
+
const rest = args2.slice(1);
|
|
6062
|
+
const nameIdx = rest.findIndex((a) => !a.startsWith("-") && a !== "--candidate");
|
|
6063
|
+
const candidateIdx = rest.indexOf("--candidate");
|
|
6064
|
+
if (nameIdx < 0 || candidateIdx < 0 || candidateIdx + 1 >= rest.length) {
|
|
6065
|
+
await platform2.write(2, "mcpcat add --candidate: requires <name> --candidate <id>\n");
|
|
6066
|
+
return 2;
|
|
6067
|
+
}
|
|
6068
|
+
const name = rest[nameIdx];
|
|
6069
|
+
const candidateId2 = rest[candidateIdx + 1];
|
|
6070
|
+
const configIdx = rest.indexOf("--config");
|
|
6071
|
+
const configFile = configIdx >= 0 && configIdx + 1 < rest.length ? rest[configIdx + 1] : void 0;
|
|
6072
|
+
return runAddCandidate(platform2, name, candidateId2, configFile);
|
|
6073
|
+
}
|
|
6074
|
+
if (parsed.verb === "discover") return runDiscover(platform2, args2.slice(1));
|
|
6075
|
+
if (parsed.verb === "inbox") return runInbox(platform2, args2.slice(1));
|
|
6076
|
+
return void 0;
|
|
6077
|
+
}
|
|
6078
|
+
async function dispatchServeVerb(platform2, args2, version) {
|
|
6079
|
+
const serveArgs = args2.slice(args2.indexOf("serve") + 1);
|
|
6080
|
+
const serveOptions = parseServeFlags(serveArgs);
|
|
6081
|
+
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
6082
|
+
await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
6083
|
+
return 2;
|
|
6084
|
+
}
|
|
6085
|
+
return runServe(platform2, serveOptions, version);
|
|
6086
|
+
}
|
|
5288
6087
|
async function run(platform2, version, args2) {
|
|
5289
6088
|
if (args2.includes("--version") || args2.includes("-V")) {
|
|
5290
6089
|
await platform2.write(1, `${version}
|
|
@@ -5305,21 +6104,19 @@ async function run(platform2, version, args2) {
|
|
|
5305
6104
|
return 2;
|
|
5306
6105
|
}
|
|
5307
6106
|
if (parsed.verb in CONFIG_VERBS) {
|
|
6107
|
+
const disc = await dispatchDiscoveryVerb(platform2, parsed, args2);
|
|
6108
|
+
if (disc !== void 0) return disc;
|
|
5308
6109
|
return dispatchConfigVerb(platform2, parsed.verb, args2);
|
|
5309
6110
|
}
|
|
6111
|
+
const discResult = await dispatchDiscoveryVerb(platform2, parsed, args2);
|
|
6112
|
+
if (discResult !== void 0) return discResult;
|
|
5310
6113
|
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
5311
6114
|
await platform2.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
5312
6115
|
`);
|
|
5313
6116
|
return 1;
|
|
5314
6117
|
}
|
|
5315
6118
|
if (parsed.verb === "serve") {
|
|
5316
|
-
|
|
5317
|
-
const serveOptions = parseServeFlags(serveArgs);
|
|
5318
|
-
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
5319
|
-
await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
5320
|
-
return 2;
|
|
5321
|
-
}
|
|
5322
|
-
return runServe(platform2, serveOptions, version);
|
|
6119
|
+
return dispatchServeVerb(platform2, args2, version);
|
|
5323
6120
|
}
|
|
5324
6121
|
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
5325
6122
|
await platform2.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
@@ -5580,5 +6377,5 @@ function createHostPlatform() {
|
|
|
5580
6377
|
// src/host.ts
|
|
5581
6378
|
var platform = createHostPlatform();
|
|
5582
6379
|
var args = process.argv.slice(2);
|
|
5583
|
-
var code = await run(platform, "0.
|
|
6380
|
+
var code = await run(platform, "0.6.0", args);
|
|
5584
6381
|
process.exit(code);
|