buddy-agent-harness 0.10.0 → 0.11.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cli.mjs +7 -0
- package/dist/index.d.mts +48 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +201 -26
- package/dist/index.mjs.map +1 -1
- package/dist/skill-scripts/doctor.mjs +1 -1
- package/dist/skill-scripts/init.mjs +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/doctor/SKILL.md +1 -1
- package/skills/doctor/scripts/doctor.mjs +1 -1
- package/skills/init/SKILL.md +1 -1
- package/skills/init/scripts/doctor.mjs +1 -1
- package/skills/init/scripts/init.mjs +1 -1
- package/skills/repair/SKILL.md +1 -1
- package/skills/repair/scripts/doctor.mjs +1 -1
- package/src/diagnose-mcp/mcp-sources.ts +8 -1
- package/src/harness-registry/harness-registry.ts +11 -0
- package/src/mcp-inventory/mcp-inventory.ts +225 -12
package/dist/cli.mjs
CHANGED
|
@@ -17876,6 +17876,13 @@ function transportOf(entry) {
|
|
|
17876
17876
|
* is a string is not a timeout, and letting it into the model would report a divergence whose real
|
|
17877
17877
|
* cause is a typo the finding does not name.
|
|
17878
17878
|
*/
|
|
17879
|
+
/**
|
|
17880
|
+
* Exported for `mcp-inventory.ts`, which reads a handful of sources this module's own `McpConfig`
|
|
17881
|
+
* shape cannot describe — a plugin's flat `.mcp.json`, a project entry nested inside `~/.claude.json`,
|
|
17882
|
+
* and hosts whose `command` is an array or an object rather than a string. Each of those normalizes
|
|
17883
|
+
* its raw entry into this same field shape before calling this function, so one reader still decides
|
|
17884
|
+
* what counts as `transport`, `args`, and the rest.
|
|
17885
|
+
*/
|
|
17879
17886
|
function serverFrom(entry) {
|
|
17880
17887
|
const args = Array.isArray(entry["args"]) && entry["args"].every((item) => typeof item === "string");
|
|
17881
17888
|
const transport = transportOf(entry);
|
package/dist/index.d.mts
CHANGED
|
@@ -110,7 +110,7 @@ type NonstandardArtifact = {
|
|
|
110
110
|
};
|
|
111
111
|
//#endregion
|
|
112
112
|
//#region src/harness-registry/harness-registry.d.ts
|
|
113
|
-
type HarnessName = "claude-code" | "cursor" | "codex" | "copilot-cli" | "gemini-cli" | "devin-desktop" | "windsurf";
|
|
113
|
+
type HarnessName = "claude-code" | "cursor" | "codex" | "copilot-cli" | "gemini-cli" | "devin-desktop" | "windsurf" | "vscode" | "opencode" | "zed";
|
|
114
114
|
/** The two scopes a harness reads configuration at. Their roots differ; their shape does not. */
|
|
115
115
|
type HarnessScopeName = "project" | "user";
|
|
116
116
|
/**
|
|
@@ -599,10 +599,46 @@ type InitializeResult = {
|
|
|
599
599
|
export declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
|
|
600
600
|
//#endregion
|
|
601
601
|
//#region src/mcp-inventory/mcp-inventory.d.ts
|
|
602
|
+
/**
|
|
603
|
+
* A public, redaction-safe inventory of configured MCP servers: what `diagnoseMcp` already knows how
|
|
604
|
+
* to read, reshaped for a consumer that wants a list rather than a drift diagnosis.
|
|
605
|
+
*
|
|
606
|
+
* This is a **different question** than `doctor` answers, not a wider version of it. `doctor`
|
|
607
|
+
* compares project-scope configuration against a golden set the user authors, and stays
|
|
608
|
+
* project-scope only by design — see `apps/web/src/content/docs/agent-configuration/mcp-servers.md`
|
|
609
|
+
* for why reading a user's home directory is a wider blast radius than diagnosis needs. An inventory
|
|
610
|
+
* has no golden set to compare against and nothing to diagnose; it answers "what MCP servers does
|
|
611
|
+
* this project, and this machine, already have configured", which is a question a consumer can only
|
|
612
|
+
* answer by reading both scopes. Adding user scope here changes nothing about what `doctor` reads:
|
|
613
|
+
* `diagnoseMcp` still touches `harness.project.mcpConfig` alone.
|
|
614
|
+
*
|
|
615
|
+
* Beyond `harnessRegistry`'s own harnesses this module also covers sources `doctor` never diagnoses
|
|
616
|
+
* at all and that carry no `harnessRegistry` entry: VS Code, Windsurf/Devin Desktop's legacy config
|
|
617
|
+
* path, OpenCode, and Zed at both scopes; Claude Code's project-local servers nested inside
|
|
618
|
+
* `~/.claude.json`; and the MCP servers an installed Claude Code plugin ships. None of this changes
|
|
619
|
+
* `selectHarnesses`, `diagnoseMcp`, or `doctor`'s project-scope-only policy — this module reads none
|
|
620
|
+
* of it through those functions.
|
|
621
|
+
*
|
|
622
|
+
* Every field that could carry a credential is reduced before it leaves this module, the same
|
|
623
|
+
* discipline `mcp-secrets.ts` documents for `doctor`'s own findings: `command` is trimmed to its
|
|
624
|
+
* basename, `args` drops any `--flag=value` pair `nonSecretArgs` flags, and `url` is trimmed to its
|
|
625
|
+
* origin and path. `env` and `headers` are not returned at all — a name alone does not say whether
|
|
626
|
+
* the value behind it is a literal or a reference, and the safe answer is not to carry the map.
|
|
627
|
+
*/
|
|
628
|
+
/**
|
|
629
|
+
* The two scopes `harnessRegistry` knows, plus the two this module adds on top of it.
|
|
630
|
+
*
|
|
631
|
+
* `local` is Claude Code's project-scoped entry inside the user-level `~/.claude.json` — a server
|
|
632
|
+
* declared for one project only, not visible to any other. `plugin` is a server an installed Claude
|
|
633
|
+
* Code plugin ships; `McpServerEntry.plugin` names which one. Neither fits `project` (there is no
|
|
634
|
+
* project-scope file) or `user` (the entry does not apply to every project), so both get their own
|
|
635
|
+
* name rather than being folded into the nearer of the two and read wrong.
|
|
636
|
+
*/
|
|
637
|
+
type McpServerScope = HarnessScopeName | "local" | "plugin";
|
|
602
638
|
/** One configured MCP server, redacted of everything that could carry a credential. */
|
|
603
639
|
type McpServerEntry = {
|
|
604
640
|
harness: HarnessName;
|
|
605
|
-
scope:
|
|
641
|
+
scope: McpServerScope;
|
|
606
642
|
/** The absolute path of the config file this entry was read from. */
|
|
607
643
|
configFile: string;
|
|
608
644
|
/** The name the server is declared under. */
|
|
@@ -615,24 +651,28 @@ type McpServerEntry = {
|
|
|
615
651
|
/** The URL's origin and path, never its query string. */
|
|
616
652
|
url: string | undefined;
|
|
617
653
|
enabled: boolean;
|
|
654
|
+
/** The plugin id this server came from, set only when `scope` is `'plugin'`. */
|
|
655
|
+
plugin?: string;
|
|
618
656
|
};
|
|
619
657
|
type ListMcpServersOptions = {
|
|
620
658
|
/** The project directory to read project-scope configuration from. */
|
|
621
659
|
projectDir: string;
|
|
622
660
|
/** Overrides `os.homedir()`, for a test that must not touch the real one. */
|
|
623
661
|
homeDir?: string;
|
|
624
|
-
/** Overrides `process.env`, for a test — read only for `CODEX_HOME` and `
|
|
662
|
+
/** Overrides `process.env`, for a test — read only for `CODEX_HOME`, `COPILOT_HOME`, and `XDG_CONFIG_HOME`. */
|
|
625
663
|
env?: Readonly<Record<string, string | undefined>>;
|
|
664
|
+
/** Overrides `os.platform()`, so the VS Code user directory is testable on every OS. */
|
|
665
|
+
platform?: NodeJS.Platform;
|
|
626
666
|
};
|
|
627
667
|
/**
|
|
628
668
|
* Every MCP server configured for a project, across every supported harness and every scope that
|
|
629
669
|
* harness documents — without ever surfacing a credential.
|
|
630
670
|
*
|
|
631
|
-
* Reuses `diagnoseMcp`'s own parsing (`parseTarget`), model (`McpServer`), and
|
|
632
|
-
* (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading
|
|
633
|
-
* re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one
|
|
634
|
-
* file does not parse, contributes no entries; this function reports an inventory, not a
|
|
635
|
-
* so it has nothing to say about why a file is missing or broken.
|
|
671
|
+
* Reuses `diagnoseMcp`'s own parsing (`parseTarget`, `serverFrom`), model (`McpServer`), and
|
|
672
|
+
* redaction (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading
|
|
673
|
+
* or re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one
|
|
674
|
+
* whose file does not parse, contributes no entries; this function reports an inventory, not a
|
|
675
|
+
* diagnosis, so it has nothing to say about why a file is missing or broken.
|
|
636
676
|
*/
|
|
637
677
|
export declare function listMcpServers(options: ListMcpServersOptions): McpServerEntry[];
|
|
638
678
|
//#endregion
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/cli.ts","../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts","../src/mcp-inventory/mcp-inventory.ts"],"mappings":";;;;;;;;;;;;;wBAiDsB,IAAI,iBAAiB;;;;;;;;;;;;;;;KCrC/B;;;;;;EAOT;;EAEA;;;EAIA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;KCbS;;EAEX;;;;;;EAMA;;;;;EAKA;;;;;;EAMA;;;;;;;;;;;;;;;;;KCtBW;KAgBA;;;;;EAKX;;EAEA;EACA,MAAM;;;;KC/BK;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/cli.ts","../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts","../src/mcp-inventory/mcp-inventory.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1564,6 +1564,13 @@ function transportOf(entry) {
|
|
|
1564
1564
|
* is a string is not a timeout, and letting it into the model would report a divergence whose real
|
|
1565
1565
|
* cause is a typo the finding does not name.
|
|
1566
1566
|
*/
|
|
1567
|
+
/**
|
|
1568
|
+
* Exported for `mcp-inventory.ts`, which reads a handful of sources this module's own `McpConfig`
|
|
1569
|
+
* shape cannot describe — a plugin's flat `.mcp.json`, a project entry nested inside `~/.claude.json`,
|
|
1570
|
+
* and hosts whose `command` is an array or an object rather than a string. Each of those normalizes
|
|
1571
|
+
* its raw entry into this same field shape before calling this function, so one reader still decides
|
|
1572
|
+
* what counts as `transport`, `args`, and the rest.
|
|
1573
|
+
*/
|
|
1567
1574
|
function serverFrom(entry) {
|
|
1568
1575
|
const args = Array.isArray(entry["args"]) && entry["args"].every((item) => typeof item === "string");
|
|
1569
1576
|
const transport = transportOf(entry);
|
|
@@ -2740,25 +2747,6 @@ async function run(argv) {
|
|
|
2740
2747
|
}
|
|
2741
2748
|
//#endregion
|
|
2742
2749
|
//#region src/mcp-inventory/mcp-inventory.ts
|
|
2743
|
-
/**
|
|
2744
|
-
* A public, redaction-safe inventory of configured MCP servers: what `diagnoseMcp` already knows how
|
|
2745
|
-
* to read, reshaped for a consumer that wants a list rather than a drift diagnosis.
|
|
2746
|
-
*
|
|
2747
|
-
* This is a **different question** than `doctor` answers, not a wider version of it. `doctor`
|
|
2748
|
-
* compares project-scope configuration against a golden set the user authors, and stays
|
|
2749
|
-
* project-scope only by design — see `apps/web/src/content/docs/agent-configuration/mcp-servers.md`
|
|
2750
|
-
* for why reading a user's home directory is a wider blast radius than diagnosis needs. An inventory
|
|
2751
|
-
* has no golden set to compare against and nothing to diagnose; it answers "what MCP servers does
|
|
2752
|
-
* this project, and this machine, already have configured", which is a question a consumer can only
|
|
2753
|
-
* answer by reading both scopes. Adding user scope here changes nothing about what `doctor` reads:
|
|
2754
|
-
* `diagnoseMcp` still touches `harness.project.mcpConfig` alone.
|
|
2755
|
-
*
|
|
2756
|
-
* Every field that could carry a credential is reduced before it leaves this module, the same
|
|
2757
|
-
* discipline `mcp-secrets.ts` documents for `doctor`'s own findings: `command` is trimmed to its
|
|
2758
|
-
* basename, `args` drops any `--flag=value` pair `nonSecretArgs` flags, and `url` is trimmed to its
|
|
2759
|
-
* origin and path. `env` and `headers` are not returned at all — a name alone does not say whether
|
|
2760
|
-
* the value behind it is a literal or a reference, and the safe answer is not to carry the map.
|
|
2761
|
-
*/
|
|
2762
2750
|
/** Where a harness keeps its **user-scope** MCP configuration, as that harness documents it.
|
|
2763
2751
|
*
|
|
2764
2752
|
* Unlike `harnessRegistry`, this table is read only from here: it is not wired into `doctor`, and
|
|
@@ -2826,7 +2814,7 @@ function safeUrl(value) {
|
|
|
2826
2814
|
return;
|
|
2827
2815
|
}
|
|
2828
2816
|
}
|
|
2829
|
-
function toEntry(harness, scope, file, name, server) {
|
|
2817
|
+
function toEntry(harness, scope, file, name, server, plugin) {
|
|
2830
2818
|
return {
|
|
2831
2819
|
harness,
|
|
2832
2820
|
scope,
|
|
@@ -2836,7 +2824,8 @@ function toEntry(harness, scope, file, name, server) {
|
|
|
2836
2824
|
command: server.command ? basename(server.command) : void 0,
|
|
2837
2825
|
args: nonSecretArgs(server.args),
|
|
2838
2826
|
url: safeUrl(server.url),
|
|
2839
|
-
enabled: server.enabled !== false
|
|
2827
|
+
enabled: server.enabled !== false,
|
|
2828
|
+
...plugin ? { plugin } : {}
|
|
2840
2829
|
};
|
|
2841
2830
|
}
|
|
2842
2831
|
function entriesFrom(harness, scope, file, config) {
|
|
@@ -2845,18 +2834,195 @@ function entriesFrom(harness, scope, file, config) {
|
|
|
2845
2834
|
return [...parsed.servers].map(([name, server]) => toEntry(harness, scope, file, name, server));
|
|
2846
2835
|
}
|
|
2847
2836
|
/**
|
|
2837
|
+
* A raw entry reshaped so `serverFrom` recognizes fields the registry-driven harnesses never write:
|
|
2838
|
+
* OpenCode's `command` as an array (its first element is the command, the rest are args), Zed's
|
|
2839
|
+
* `command` as `{ path, args }`, and Windsurf's `serverUrl` as an alias for `url`. A field none of
|
|
2840
|
+
* those apply to is passed through unchanged.
|
|
2841
|
+
*/
|
|
2842
|
+
function normalizeRawEntry(raw) {
|
|
2843
|
+
const command = raw["command"];
|
|
2844
|
+
if (Array.isArray(command)) {
|
|
2845
|
+
const [first, ...rest] = command;
|
|
2846
|
+
return {
|
|
2847
|
+
...raw,
|
|
2848
|
+
command: first,
|
|
2849
|
+
args: raw["args"] ?? rest
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
if (isRecord(command)) return {
|
|
2853
|
+
...raw,
|
|
2854
|
+
command: command["path"],
|
|
2855
|
+
args: raw["args"] ?? command["args"]
|
|
2856
|
+
};
|
|
2857
|
+
if (raw["url"] === void 0 && typeof raw["serverUrl"] === "string") return {
|
|
2858
|
+
...raw,
|
|
2859
|
+
url: raw["serverUrl"]
|
|
2860
|
+
};
|
|
2861
|
+
return raw;
|
|
2862
|
+
}
|
|
2863
|
+
/** Every server under `key` in an already-parsed JSON(C) document, normalized and converted. */
|
|
2864
|
+
function serversFromDocument(document, key) {
|
|
2865
|
+
const table = isRecord(document) ? document[key] : void 0;
|
|
2866
|
+
if (!isRecord(table)) return /* @__PURE__ */ new Map();
|
|
2867
|
+
return new Map(Object.entries(table).filter((pair) => isRecord(pair[1])).map(([name, entry]) => [name, serverFrom(normalizeRawEntry(entry))]));
|
|
2868
|
+
}
|
|
2869
|
+
function entriesFromDocument(harness, scope, file, document, key, options = {}) {
|
|
2870
|
+
return [...serversFromDocument(document, key)].map(([name, server]) => {
|
|
2871
|
+
const entry = toEntry(harness, scope, file, name, server, options.plugin);
|
|
2872
|
+
return options.forceDisabled ? {
|
|
2873
|
+
...entry,
|
|
2874
|
+
enabled: false
|
|
2875
|
+
} : entry;
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
/** Where VS Code keeps its user-scope settings, per platform — mirrors what VS Code itself documents. */
|
|
2879
|
+
function vscodeUserDir(env, home, plat) {
|
|
2880
|
+
if (plat === "win32") return join(env["APPDATA"] || join(home, "AppData", "Roaming"), "Code", "User");
|
|
2881
|
+
if (plat === "darwin") return join(home, "Library", "Application Support", "Code", "User");
|
|
2882
|
+
return join(env["XDG_CONFIG_HOME"] || join(home, ".config"), "Code", "User");
|
|
2883
|
+
}
|
|
2884
|
+
/**
|
|
2885
|
+
* Sources `harnessRegistry` carries no entry for at all, so they cannot be read through
|
|
2886
|
+
* `userScopeConfig` or a harness's `project.mcpConfig`. Each of these harnesses is either not one
|
|
2887
|
+
* `doctor` diagnoses (VS Code, OpenCode, Zed have no `HarnessScope.mcpConfig`) or is diagnosed under
|
|
2888
|
+
* a different name than the one its own vendor uses for this file: Windsurf was rebranded to Devin
|
|
2889
|
+
* Desktop (`.research/agentic-configuration-standards/evidence.md`, E-WS-02), and
|
|
2890
|
+
* `~/.codeium/windsurf/mcp_config.json` is that product's legacy config path, read here under
|
|
2891
|
+
* `devin-desktop` rather than under a separate `windsurf` MCP source. This is a deliberate departure
|
|
2892
|
+
* from `repobuddy`'s scanner, which has no such mapping and reports the file under a standalone
|
|
2893
|
+
* `windsurf` harness name.
|
|
2894
|
+
*/
|
|
2895
|
+
function extraSources(projectDir, home, env, plat) {
|
|
2896
|
+
const xdg = env["XDG_CONFIG_HOME"] || join(home, ".config");
|
|
2897
|
+
return [
|
|
2898
|
+
{
|
|
2899
|
+
harness: "vscode",
|
|
2900
|
+
scope: "user",
|
|
2901
|
+
file: join(vscodeUserDir(env, home, plat), "mcp.json"),
|
|
2902
|
+
key: "servers"
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
harness: "vscode",
|
|
2906
|
+
scope: "project",
|
|
2907
|
+
file: join(projectDir, ".vscode", "mcp.json"),
|
|
2908
|
+
key: "servers"
|
|
2909
|
+
},
|
|
2910
|
+
{
|
|
2911
|
+
harness: "devin-desktop",
|
|
2912
|
+
scope: "user",
|
|
2913
|
+
file: join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2914
|
+
key: "mcpServers"
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
harness: "opencode",
|
|
2918
|
+
scope: "user",
|
|
2919
|
+
file: join(xdg, "opencode", "opencode.json"),
|
|
2920
|
+
key: "mcp"
|
|
2921
|
+
},
|
|
2922
|
+
{
|
|
2923
|
+
harness: "opencode",
|
|
2924
|
+
scope: "project",
|
|
2925
|
+
file: join(projectDir, "opencode.json"),
|
|
2926
|
+
key: "mcp"
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
harness: "zed",
|
|
2930
|
+
scope: "user",
|
|
2931
|
+
file: join(xdg, "zed", "settings.json"),
|
|
2932
|
+
key: "context_servers"
|
|
2933
|
+
},
|
|
2934
|
+
{
|
|
2935
|
+
harness: "zed",
|
|
2936
|
+
scope: "project",
|
|
2937
|
+
file: join(projectDir, ".zed", "settings.json"),
|
|
2938
|
+
key: "context_servers"
|
|
2939
|
+
}
|
|
2940
|
+
];
|
|
2941
|
+
}
|
|
2942
|
+
/**
|
|
2943
|
+
* Claude Code's project-local servers: `~/.claude.json`'s `projects["<absolute project dir>"]
|
|
2944
|
+
* .mcpServers`, a server declared for this one project rather than every project the way the file's
|
|
2945
|
+
* top-level `mcpServers` (read as `'user'` scope, above) is.
|
|
2946
|
+
*/
|
|
2947
|
+
function claudeLocalEntries(home, projectDir) {
|
|
2948
|
+
const file = join(home, ".claude.json");
|
|
2949
|
+
const text = read(file);
|
|
2950
|
+
if (text === void 0) return [];
|
|
2951
|
+
const document = parseJsonWithComments(text);
|
|
2952
|
+
const projects = isRecord(document) ? document["projects"] : void 0;
|
|
2953
|
+
const project = isRecord(projects) ? projects[resolve(projectDir)] : void 0;
|
|
2954
|
+
if (!isRecord(project)) return [];
|
|
2955
|
+
return entriesFromDocument("claude-code", "local", file, project, "mcpServers");
|
|
2956
|
+
}
|
|
2957
|
+
/** `enabledPlugins`, merged from global settings up through project-local settings, last write wins. */
|
|
2958
|
+
function enabledPlugins(home, projectDir) {
|
|
2959
|
+
const enabled = {};
|
|
2960
|
+
for (const file of [
|
|
2961
|
+
join(home, ".claude", "settings.json"),
|
|
2962
|
+
join(projectDir, ".claude", "settings.json"),
|
|
2963
|
+
join(projectDir, ".claude", "settings.local.json")
|
|
2964
|
+
]) {
|
|
2965
|
+
const text = read(file);
|
|
2966
|
+
if (text === void 0) continue;
|
|
2967
|
+
const document = parseJsonWithComments(text);
|
|
2968
|
+
const table = isRecord(document) ? document["enabledPlugins"] : void 0;
|
|
2969
|
+
if (!isRecord(table)) continue;
|
|
2970
|
+
for (const [id, value] of Object.entries(table)) if (typeof value === "boolean") enabled[id] = value;
|
|
2971
|
+
}
|
|
2972
|
+
return enabled;
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* MCP servers shipped by installed Claude Code plugins, with `enabled` reflecting whether the plugin
|
|
2976
|
+
* itself is enabled for this project — a plugin not set to `true` in the merged `enabledPlugins` is
|
|
2977
|
+
* disabled, and every server it ships is reported disabled regardless of that server's own field. An
|
|
2978
|
+
* install whose `projectPath` names a different project contributes nothing: that install is not
|
|
2979
|
+
* this project's.
|
|
2980
|
+
*/
|
|
2981
|
+
function pluginEntries(home, projectDir) {
|
|
2982
|
+
const text = read(join(home, ".claude", "plugins", "installed_plugins.json"));
|
|
2983
|
+
if (text === void 0) return [];
|
|
2984
|
+
const document = parseJsonWithComments(text);
|
|
2985
|
+
const installed = isRecord(document) ? document["plugins"] : void 0;
|
|
2986
|
+
if (!isRecord(installed)) return [];
|
|
2987
|
+
const enabled = enabledPlugins(home, projectDir);
|
|
2988
|
+
const absoluteProject = resolve(projectDir);
|
|
2989
|
+
const entries = [];
|
|
2990
|
+
for (const [id, list] of Object.entries(installed)) {
|
|
2991
|
+
if (!Array.isArray(list)) continue;
|
|
2992
|
+
for (const install of list) {
|
|
2993
|
+
if (!isRecord(install)) continue;
|
|
2994
|
+
const projectPath = install["projectPath"];
|
|
2995
|
+
if (typeof projectPath === "string" && resolve(projectPath) !== absoluteProject) continue;
|
|
2996
|
+
const installPath = install["installPath"];
|
|
2997
|
+
if (typeof installPath !== "string") continue;
|
|
2998
|
+
const mcpFile = join(installPath, ".mcp.json");
|
|
2999
|
+
const mcpText = read(mcpFile);
|
|
3000
|
+
if (mcpText === void 0) continue;
|
|
3001
|
+
const mcpDocument = parseJsonWithComments(mcpText);
|
|
3002
|
+
if (!isRecord(mcpDocument)) continue;
|
|
3003
|
+
const document2 = isRecord(mcpDocument["mcpServers"]) ? mcpDocument : { mcpServers: mcpDocument };
|
|
3004
|
+
entries.push(...entriesFromDocument("claude-code", "plugin", mcpFile, document2, "mcpServers", {
|
|
3005
|
+
plugin: id,
|
|
3006
|
+
forceDisabled: enabled[id] !== true
|
|
3007
|
+
}));
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
return entries;
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
2848
3013
|
* Every MCP server configured for a project, across every supported harness and every scope that
|
|
2849
3014
|
* harness documents — without ever surfacing a credential.
|
|
2850
3015
|
*
|
|
2851
|
-
* Reuses `diagnoseMcp`'s own parsing (`parseTarget`), model (`McpServer`), and
|
|
2852
|
-
* (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading
|
|
2853
|
-
* re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one
|
|
2854
|
-
* file does not parse, contributes no entries; this function reports an inventory, not a
|
|
2855
|
-
* so it has nothing to say about why a file is missing or broken.
|
|
3016
|
+
* Reuses `diagnoseMcp`'s own parsing (`parseTarget`, `serverFrom`), model (`McpServer`), and
|
|
3017
|
+
* redaction (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading
|
|
3018
|
+
* or re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one
|
|
3019
|
+
* whose file does not parse, contributes no entries; this function reports an inventory, not a
|
|
3020
|
+
* diagnosis, so it has nothing to say about why a file is missing or broken.
|
|
2856
3021
|
*/
|
|
2857
3022
|
function listMcpServers(options) {
|
|
2858
3023
|
const home = options.homeDir ?? homedir();
|
|
2859
3024
|
const env = options.env ?? process.env;
|
|
3025
|
+
const plat = options.platform ?? platform();
|
|
2860
3026
|
const entries = [];
|
|
2861
3027
|
for (const harness of harnessRegistry) {
|
|
2862
3028
|
if (harness.deprecated) continue;
|
|
@@ -2865,6 +3031,15 @@ function listMcpServers(options) {
|
|
|
2865
3031
|
const user = userScopeConfig(harness.name, home, env);
|
|
2866
3032
|
if (user) entries.push(...entriesFrom(harness.name, "user", user.file, user.config));
|
|
2867
3033
|
}
|
|
3034
|
+
entries.push(...claudeLocalEntries(home, options.projectDir));
|
|
3035
|
+
entries.push(...pluginEntries(home, options.projectDir));
|
|
3036
|
+
for (const source of extraSources(options.projectDir, home, env, plat)) {
|
|
3037
|
+
const text = read(source.file);
|
|
3038
|
+
if (text === void 0) continue;
|
|
3039
|
+
const document = parseJsonWithComments(text);
|
|
3040
|
+
if (document === void 0) continue;
|
|
3041
|
+
entries.push(...entriesFromDocument(source.harness, source.scope, source.file, document, source.key));
|
|
3042
|
+
}
|
|
2868
3043
|
return entries;
|
|
2869
3044
|
}
|
|
2870
3045
|
//#endregion
|