buddy-agent-harness 0.9.1 → 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 +80 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +326 -6
- 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-secrets.ts +17 -0
- package/src/diagnose-mcp/mcp-sources.ts +8 -1
- package/src/harness-registry/harness-registry.ts +11 -0
- package/src/index.ts +2 -0
- package/src/mcp-inventory/mcp-inventory.ts +378 -0
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
|
/**
|
|
@@ -598,5 +598,83 @@ type InitializeResult = {
|
|
|
598
598
|
};
|
|
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";
|
|
638
|
+
/** One configured MCP server, redacted of everything that could carry a credential. */
|
|
639
|
+
type McpServerEntry = {
|
|
640
|
+
harness: HarnessName;
|
|
641
|
+
scope: McpServerScope;
|
|
642
|
+
/** The absolute path of the config file this entry was read from. */
|
|
643
|
+
configFile: string;
|
|
644
|
+
/** The name the server is declared under. */
|
|
645
|
+
name: string;
|
|
646
|
+
transport: McpTransport | undefined;
|
|
647
|
+
/** The command's basename, never its full path. */
|
|
648
|
+
command: string | undefined;
|
|
649
|
+
/** `args`, with any credential-bearing `--flag=value` pair dropped. */
|
|
650
|
+
args: readonly string[];
|
|
651
|
+
/** The URL's origin and path, never its query string. */
|
|
652
|
+
url: string | undefined;
|
|
653
|
+
enabled: boolean;
|
|
654
|
+
/** The plugin id this server came from, set only when `scope` is `'plugin'`. */
|
|
655
|
+
plugin?: string;
|
|
656
|
+
};
|
|
657
|
+
type ListMcpServersOptions = {
|
|
658
|
+
/** The project directory to read project-scope configuration from. */
|
|
659
|
+
projectDir: string;
|
|
660
|
+
/** Overrides `os.homedir()`, for a test that must not touch the real one. */
|
|
661
|
+
homeDir?: string;
|
|
662
|
+
/** Overrides `process.env`, for a test — read only for `CODEX_HOME`, `COPILOT_HOME`, and `XDG_CONFIG_HOME`. */
|
|
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;
|
|
666
|
+
};
|
|
667
|
+
/**
|
|
668
|
+
* Every MCP server configured for a project, across every supported harness and every scope that
|
|
669
|
+
* harness documents — without ever surfacing a credential.
|
|
670
|
+
*
|
|
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.
|
|
676
|
+
*/
|
|
677
|
+
export declare function listMcpServers(options: ListMcpServersOptions): McpServerEntry[];
|
|
678
|
+
//#endregion
|
|
679
|
+
export type { BridgeFinding, BridgeKind, BridgeProblem, BridgeReport, BridgeStatus, ConfigurationFault, ConfigurationProblem, DiagnoseMcpOptions, DiagnoseOptions, DiagnoseResult, DivergenceDirection, DivergenceReport, DoctorProblem, DoctorReport, Harness, HarnessName, HarnessScope, HarnessScopeName, InitializeOptions, InitializeResult, InstructionBridge, InstructionKind, InstructionProblem, InstructionReport, InstructionStatus, ListMcpServersOptions, McpConfig, McpDirection, McpField, McpProblem, McpServer, McpServerEntry, McpTransport, Repair };
|
|
602
680
|
//# sourceMappingURL=index.d.mts.map
|
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"],"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
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { cli, command, exitCodes, z } from "clibuilder";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
|
-
import { dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { encode } from "@toon-format/toon";
|
|
7
7
|
import { parse } from "smol-toml";
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
@@ -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);
|
|
@@ -1831,9 +1838,25 @@ function credentialFields(server) {
|
|
|
1831
1838
|
...server.url && urlHoldsCredential(server.url) ? ["url"] : []
|
|
1832
1839
|
];
|
|
1833
1840
|
}
|
|
1841
|
+
/**
|
|
1842
|
+
* `args` with any `--flag=value` pair dropped whose flag names a credential and whose value is a
|
|
1843
|
+
* literal rather than a reference — the same test `credentialKey` and `holdsLiteral` apply to `env`
|
|
1844
|
+
* and `headers`, applied here to the one field that carries names and values in a single string
|
|
1845
|
+
* instead of a map.
|
|
1846
|
+
*
|
|
1847
|
+
* A positional argument, or a flag with no `=`, passes through unfiltered: `-y` and a package name
|
|
1848
|
+
* are not a place a credential's value is written, and there is no key in either to test.
|
|
1849
|
+
*/
|
|
1850
|
+
function nonSecretArgs(args) {
|
|
1851
|
+
return (args ?? []).filter((arg) => {
|
|
1852
|
+
const at = arg.indexOf("=");
|
|
1853
|
+
if (at === -1) return true;
|
|
1854
|
+
return !(credentialKey(arg.slice(0, at)) && holdsLiteral(arg.slice(at + 1)));
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1834
1857
|
//#endregion
|
|
1835
1858
|
//#region src/diagnose-mcp/diagnose-mcp.ts
|
|
1836
|
-
function read(root, path) {
|
|
1859
|
+
function read$1(root, path) {
|
|
1837
1860
|
const absolute = join(root, path);
|
|
1838
1861
|
return existsSync(absolute) ? readFileSync(absolute, "utf8") : void 0;
|
|
1839
1862
|
}
|
|
@@ -1884,7 +1907,7 @@ function diagnoseMcp({ root, git, cli }) {
|
|
|
1884
1907
|
const targets = /* @__PURE__ */ new Map();
|
|
1885
1908
|
const configs = [];
|
|
1886
1909
|
for (const config of targetsOf(root)) {
|
|
1887
|
-
const parsed = parseTarget(config, read(root, config.path));
|
|
1910
|
+
const parsed = parseTarget(config, read$1(root, config.path));
|
|
1888
1911
|
if (parsed.kind === "unreadable") {
|
|
1889
1912
|
add({ file: config.path }, "mcp-target-unreadable");
|
|
1890
1913
|
continue;
|
|
@@ -1894,7 +1917,7 @@ function diagnoseMcp({ root, git, cli }) {
|
|
|
1894
1917
|
targets.set(config.path, parsed.servers);
|
|
1895
1918
|
configs.push(config);
|
|
1896
1919
|
}
|
|
1897
|
-
const golden = parseGoldenSet(read(root, goldenSetPath));
|
|
1920
|
+
const golden = parseGoldenSet(read$1(root, goldenSetPath));
|
|
1898
1921
|
if (golden.kind === "unreadable") {
|
|
1899
1922
|
add({
|
|
1900
1923
|
file: goldenSetPath,
|
|
@@ -1906,7 +1929,7 @@ function diagnoseMcp({ root, git, cli }) {
|
|
|
1906
1929
|
reportSecrets(goldenSetPath, golden.servers);
|
|
1907
1930
|
const baseline = new McpBaseline({
|
|
1908
1931
|
git,
|
|
1909
|
-
record: parseProjectionRecord(read(root, projectionRecordPath))
|
|
1932
|
+
record: parseProjectionRecord(read$1(root, projectionRecordPath))
|
|
1910
1933
|
});
|
|
1911
1934
|
for (const config of configs) {
|
|
1912
1935
|
const servers = targets.get(config.path);
|
|
@@ -2723,6 +2746,303 @@ async function run(argv) {
|
|
|
2723
2746
|
}
|
|
2724
2747
|
}
|
|
2725
2748
|
//#endregion
|
|
2726
|
-
|
|
2749
|
+
//#region src/mcp-inventory/mcp-inventory.ts
|
|
2750
|
+
/** Where a harness keeps its **user-scope** MCP configuration, as that harness documents it.
|
|
2751
|
+
*
|
|
2752
|
+
* Unlike `harnessRegistry`, this table is read only from here: it is not wired into `doctor`, and
|
|
2753
|
+
* adding a harness to it never changes what `doctor` reads. Codex and Copilot CLI each let their
|
|
2754
|
+
* whole configuration directory move via an environment variable — `CODEX_HOME` and `COPILOT_HOME`
|
|
2755
|
+
* — so those two resolve through `env` before falling back to the home directory; the rest do not
|
|
2756
|
+
* document one.
|
|
2757
|
+
*/
|
|
2758
|
+
function userScopeConfig(harness, home, env) {
|
|
2759
|
+
switch (harness) {
|
|
2760
|
+
case "claude-code": return {
|
|
2761
|
+
file: join(home, ".claude.json"),
|
|
2762
|
+
config: {
|
|
2763
|
+
path: ".claude.json",
|
|
2764
|
+
key: "mcpServers",
|
|
2765
|
+
format: "json"
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
case "cursor": return {
|
|
2769
|
+
file: join(home, ".cursor", "mcp.json"),
|
|
2770
|
+
config: {
|
|
2771
|
+
path: ".cursor/mcp.json",
|
|
2772
|
+
key: "mcpServers",
|
|
2773
|
+
format: "json"
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
case "codex": return {
|
|
2777
|
+
file: join(env["CODEX_HOME"] || join(home, ".codex"), "config.toml"),
|
|
2778
|
+
config: {
|
|
2779
|
+
path: "config.toml",
|
|
2780
|
+
key: "mcp_servers",
|
|
2781
|
+
format: "toml"
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
case "copilot-cli": return {
|
|
2785
|
+
file: join(env["COPILOT_HOME"] || join(home, ".copilot"), "mcp-config.json"),
|
|
2786
|
+
config: {
|
|
2787
|
+
path: "mcp-config.json",
|
|
2788
|
+
key: "mcpServers",
|
|
2789
|
+
format: "json"
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2792
|
+
case "gemini-cli": return {
|
|
2793
|
+
file: join(home, ".gemini", "settings.json"),
|
|
2794
|
+
config: {
|
|
2795
|
+
path: ".gemini/settings.json",
|
|
2796
|
+
key: "mcpServers",
|
|
2797
|
+
format: "json",
|
|
2798
|
+
shared: true
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
default: return;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
function read(file) {
|
|
2805
|
+
return existsSync(file) ? readFileSync(file, "utf8") : void 0;
|
|
2806
|
+
}
|
|
2807
|
+
/** A URL trimmed to its origin and path, dropping any query string a token could ride in on. */
|
|
2808
|
+
function safeUrl(value) {
|
|
2809
|
+
if (!value) return void 0;
|
|
2810
|
+
try {
|
|
2811
|
+
const url = new URL(value);
|
|
2812
|
+
return `${url.origin}${url.pathname}`;
|
|
2813
|
+
} catch {
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
function toEntry(harness, scope, file, name, server, plugin) {
|
|
2818
|
+
return {
|
|
2819
|
+
harness,
|
|
2820
|
+
scope,
|
|
2821
|
+
configFile: file,
|
|
2822
|
+
name,
|
|
2823
|
+
transport: server.transport,
|
|
2824
|
+
command: server.command ? basename(server.command) : void 0,
|
|
2825
|
+
args: nonSecretArgs(server.args),
|
|
2826
|
+
url: safeUrl(server.url),
|
|
2827
|
+
enabled: server.enabled !== false,
|
|
2828
|
+
...plugin ? { plugin } : {}
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function entriesFrom(harness, scope, file, config) {
|
|
2832
|
+
const parsed = parseTarget(config, read(file));
|
|
2833
|
+
if (parsed.kind !== "servers") return [];
|
|
2834
|
+
return [...parsed.servers].map(([name, server]) => toEntry(harness, scope, file, name, server));
|
|
2835
|
+
}
|
|
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
|
+
/**
|
|
3013
|
+
* Every MCP server configured for a project, across every supported harness and every scope that
|
|
3014
|
+
* harness documents — without ever surfacing a credential.
|
|
3015
|
+
*
|
|
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.
|
|
3021
|
+
*/
|
|
3022
|
+
function listMcpServers(options) {
|
|
3023
|
+
const home = options.homeDir ?? homedir();
|
|
3024
|
+
const env = options.env ?? process.env;
|
|
3025
|
+
const plat = options.platform ?? platform();
|
|
3026
|
+
const entries = [];
|
|
3027
|
+
for (const harness of harnessRegistry) {
|
|
3028
|
+
if (harness.deprecated) continue;
|
|
3029
|
+
const project = harness.project.mcpConfig;
|
|
3030
|
+
if (project) entries.push(...entriesFrom(harness.name, "project", join(options.projectDir, project.path), project));
|
|
3031
|
+
const user = userScopeConfig(harness.name, home, env);
|
|
3032
|
+
if (user) entries.push(...entriesFrom(harness.name, "user", user.file, user.config));
|
|
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
|
+
}
|
|
3043
|
+
return entries;
|
|
3044
|
+
}
|
|
3045
|
+
//#endregion
|
|
3046
|
+
export { activate, bridgeRepairs, buildDoctorReport, diagnoseBridges, diagnoseInstructions, diagnoseMcp, doctorCommand, doctorRepairs, doctorSkill, goldenSetPath, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, listMcpServers, renderDoctorSkill, run };
|
|
2727
3047
|
|
|
2728
3048
|
//# sourceMappingURL=index.mjs.map
|