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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "author": {
6
6
  "name": "repobuddy"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "author": {
6
6
  "name": "repobuddy"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "author": {
6
6
  "name": "repobuddy"
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: HarnessScopeName;
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 `COPILOT_HOME`. */
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 redaction
632
- * (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading or
633
- * re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one whose
634
- * file does not parse, contributes no entries; this function reports an inventory, not a diagnosis,
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
@@ -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":"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"}
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;;KAqBA;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA;EACX;EACA;EACA,oBAAoB;EACpB,YAAY;EACZ,uBAAuB;;KAGZ;EACX,MAAM;;EAEN,SAAS;;;;;EAKT,OAAO;;EAEP,aAAa;;;;;;;;;;;;;;;;;;;;;;qBAuBD,0BAA0B;;;;KCpF3B;;;;;;KAMA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;;;;;;;;;wBAkFO,qBACf,cACA,oBAAoB,WACpB;EACI,cAAc;EAAqB,UAAU;;;;;;;;;;;;;;;;;;;;KC5FtC;EAAa;EAAc;;KAE3B;;EAEX;;EAEA;;EAEA;;EAEA,WAAW;;;;;;;;;;KCbA;;;;;;;;;;;KAqBA;;;;;;;;;KAUA;;;;;;;;;KAoBA;KAOA;;KAOA,gBAAgB,gBAAgB,qBAAqB,qBAAqB,aAAa;;KAGvF,uBAAuB;;;;;;;;;;;;;;;;;;KAmBvB;;EAEX;;EAEA;;KAGW;EACX,SAAS;;EAET;;;;;;;EAOA,OAAO,IAAI,SAAS,cAAc;;;;;;EAMlC,YAAY,IAAI;;qBAyLJ,wBAAwB;qBA4CxB,6BAA6B;qBAuM7B,wBAAwB;qBAOxB;WACZ;WACA;;;;;;;;;;wBAmCe,kBAAkB;;;;KC/kBtB;;KAGA;;;;;;cAwBC;mBAIA;;mBAFK;EAEjB,YAAY;;;;;;EASZ,WAAW,eAAe;;;;;;;EAa1B,UAAU;;;;;;;;;EAaV,gBAAgB;;EAMhB,UAAU,gBAAgB;;;;;EAS1B,YAAY,gBAAgB,oBAAoB;;UAYxC;;;;;UAmBA;;;;KC5GG;KACA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;KAGG;EACX;;;;;EAKA,SAAS;EACT;;EAEA,QAAQ;;KAGG;EACX;EACA,WAAW;;KAGA;EACX;EACA,YAAY;;EAEZ;;KAGW;EACX,SAAS;;;;;EAKT,cAAc;EACd,YAAY;EACZ,UAAU;;;;;;;wBA2EK,kBAAkB,MAAM,WAAW,WAAgB,OAAO,kBAAkB;;;;;;;;;;;;;;;;;KC1GhF;;EAEX;EACA,SAAS;EACT;;EAEA,QAAQ;;;;KCfG;EACX;EACA,SAAS;EACT,cAAc;EACd,aAAa;;;;;EAKb;IAAY;IAAc,SAAS;IAAe;;;;;;;;;;EASlD,OAAO;;;;;;wBAOQ,kBACf,aACA,QAAQ,gBACR,gBAAe,yBACb;qBAgCU,eAAe,MAAI;;;;;;;;;;;;;;;;;;KC7CpB;EACX;EACA,KAAK;;EAEL;;wBAgCe,cAAc,MAAM,KAAK,OAAO,qBAAqB;;;;;;;;;;;;;;;;KClDzD;KAEA;EACX,YAAY;EACZ;EACA;EACA,MAAM,SAAS;EACf;EACA,UAAU,SAAS;EACnB;EACA;EACA;EACA;;;cAIY;KAaD,mBAAmB;;;;KCnCnB;;;;qBCAC;;;qBCAA,aAAa,MAAI;qBAoDjB,gBAAgB,MAAI;wBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;;;;;;KCV1D;;;KCpDA;EACX;EACA,YAAY;EACZ;EACA,QAAQ;;KAEG;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER,SAAS;;EAET;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;wBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KCmBX,iBAAiB;;KA2DjB;EACX,SAAS;EACT,OAAO;;EAEP;;EAEA;EACA,WAAW;;EAEX;;EAEA;;EAEA;EACA;;EAEA;;KAGW;;EAEX;;EAEA;;EAEA,MAAM,SAAS;;EAEf,WAAW,OAAO;;;;;;;;;;;;wBAwNH,eAAe,SAAS,wBAAwB"}
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 redaction
2852
- * (`nonSecretArgs`, and the URL/command trimming this module adds) rather than re-reading or
2853
- * re-parsing any of the supported harnesses' MCP files. A harness with no file present, or one whose
2854
- * file does not parse, contributes no entries; this function reports an inventory, not a diagnosis,
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