codex-plugin-doctor 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -160,6 +160,8 @@ codex-plugin-doctor init my-plugin
160
160
  codex-plugin-doctor compat .
161
161
  codex-plugin-doctor compat . --client codex
162
162
  codex-plugin-doctor compat . --client generic-mcp
163
+ codex-plugin-doctor compat . --client claude-desktop
164
+ codex-plugin-doctor compat . --client claude-desktop --install-preview
163
165
  codex-plugin-doctor compat . --json
164
166
  codex-plugin-doctor compat . --json --output compatibility.json
165
167
  codex-plugin-doctor check .
@@ -174,6 +176,8 @@ codex-plugin-doctor check . --config .codex-doctor.json
174
176
  codex-plugin-doctor check . --json --runtime --verbose-runtime
175
177
  ```
176
178
 
179
+ `compat --client claude-desktop` checks whether the MCP package can be added to the local Claude Desktop setup. On Windows it looks for `%APPDATA%\Claude\claude_desktop_config.json`; on macOS it looks for `~/Library/Application Support/Claude/claude_desktop_config.json`. A valid existing config returns `PASS`, a missing Claude Desktop install returns `WARN`, and a malformed local config returns `FAIL` so you do not add new servers into a broken config file. If the package server name already exists in Claude Desktop, the command returns `WARN` with the duplicate server name. Add `--install-preview` to print the JSON snippet that should be merged into `claude_desktop_config.json`; it does not modify files.
180
+
177
181
  Optional local policy file:
178
182
 
179
183
  ```json
@@ -0,0 +1,10 @@
1
+ import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
2
+ export interface ClaudeDesktopInstallPreview {
3
+ targetPath: string;
4
+ configPath: string;
5
+ snippet: {
6
+ mcpServers: Record<string, unknown>;
7
+ };
8
+ }
9
+ export declare function buildClaudeDesktopInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<ClaudeDesktopInstallPreview>;
10
+ export declare function renderClaudeDesktopInstallPreview(preview: ClaudeDesktopInstallPreview): string;
@@ -0,0 +1,69 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getClaudeDesktopConfigPath, readMcpConfigPath } from "./compatibility-matrix.js";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function isRelativeLocalPath(value) {
8
+ return value.startsWith("./") ||
9
+ value.startsWith("../") ||
10
+ value.startsWith(".\\") ||
11
+ value.startsWith("..\\");
12
+ }
13
+ function normalizeLocalPathArgument(value, rootPath) {
14
+ return typeof value === "string" && isRelativeLocalPath(value)
15
+ ? path.resolve(rootPath, value)
16
+ : value;
17
+ }
18
+ function normalizeServerConfig(serverConfig, rootPath) {
19
+ if (!isRecord(serverConfig)) {
20
+ return serverConfig;
21
+ }
22
+ const normalized = { ...serverConfig };
23
+ if (typeof normalized.command === "string" && isRelativeLocalPath(normalized.command)) {
24
+ normalized.command = path.resolve(rootPath, normalized.command);
25
+ }
26
+ if (Array.isArray(normalized.args)) {
27
+ normalized.args = normalized.args.map((argument) => normalizeLocalPathArgument(argument, rootPath));
28
+ }
29
+ return normalized;
30
+ }
31
+ export async function buildClaudeDesktopInstallPreview(targetPath, environment = {}) {
32
+ const rootPath = path.resolve(targetPath);
33
+ const configPath = getClaudeDesktopConfigPath(environment);
34
+ if (!configPath) {
35
+ throw new Error("Claude Desktop config path could not be resolved on this platform.");
36
+ }
37
+ const mcpConfigPath = await readMcpConfigPath(rootPath);
38
+ if (!mcpConfigPath) {
39
+ throw new Error("No MCP config found for install preview.");
40
+ }
41
+ const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
42
+ const servers = parsed.mcpServers;
43
+ if (!isRecord(servers) || Object.keys(servers).length === 0) {
44
+ throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
45
+ }
46
+ return {
47
+ targetPath: rootPath,
48
+ configPath,
49
+ snippet: {
50
+ mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
51
+ serverName,
52
+ normalizeServerConfig(serverConfig, rootPath)
53
+ ]))
54
+ }
55
+ };
56
+ }
57
+ export function renderClaudeDesktopInstallPreview(preview) {
58
+ return [
59
+ "Claude Desktop Install Preview",
60
+ "==============================",
61
+ `Target: ${preview.targetPath}`,
62
+ `Config: ${preview.configPath}`,
63
+ "",
64
+ "Add or merge this snippet into `claude_desktop_config.json`:",
65
+ JSON.stringify(preview.snippet, null, 2),
66
+ "",
67
+ "No files were modified."
68
+ ].join("\n");
69
+ }
@@ -9,5 +9,12 @@ export interface CompatibilityMatrix {
9
9
  targetPath: string;
10
10
  results: CompatibilityResult[];
11
11
  }
12
- export declare function buildCompatibilityMatrix(targetPath: string): Promise<CompatibilityMatrix>;
12
+ export interface CompatibilityEnvironment {
13
+ env?: Record<string, string | undefined>;
14
+ platform?: NodeJS.Platform;
15
+ homedir?: string;
16
+ }
17
+ export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
18
+ export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
19
+ export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
13
20
  export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
@@ -1,4 +1,5 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { validatePlugin } from "../core/validate-plugin.js";
4
5
  async function fileExists(targetPath) {
@@ -10,6 +11,15 @@ async function fileExists(targetPath) {
10
11
  return false;
11
12
  }
12
13
  }
14
+ async function directoryExists(targetPath) {
15
+ try {
16
+ const details = await stat(targetPath);
17
+ return details.isDirectory();
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
13
23
  function statusFromCheckResult(result) {
14
24
  if (result.status === "fail") {
15
25
  return "fail";
@@ -19,7 +29,7 @@ function statusFromCheckResult(result) {
19
29
  }
20
30
  return "pass";
21
31
  }
22
- async function readMcpConfigPath(targetPath) {
32
+ export async function readMcpConfigPath(targetPath) {
23
33
  const rootPath = path.resolve(targetPath);
24
34
  const directMcpPath = path.join(rootPath, ".mcp.json");
25
35
  if (await fileExists(directMcpPath)) {
@@ -82,9 +92,119 @@ async function checkGenericMcp(targetPath) {
82
92
  };
83
93
  }
84
94
  }
85
- export async function buildCompatibilityMatrix(targetPath) {
95
+ async function readMcpServerNames(targetPath) {
96
+ const mcpConfigPath = await readMcpConfigPath(targetPath);
97
+ if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
98
+ return [];
99
+ }
100
+ try {
101
+ const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
102
+ const servers = parsed.mcpServers;
103
+ return typeof servers === "object" && servers !== null && !Array.isArray(servers)
104
+ ? Object.keys(servers)
105
+ : [];
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ }
111
+ export function getClaudeDesktopConfigPath(environment = {}) {
112
+ const platform = environment.platform ?? process.platform;
113
+ const env = environment.env ?? process.env;
114
+ const homeDirectory = environment.homedir ?? os.homedir();
115
+ if (platform === "win32") {
116
+ const appData = env.APPDATA;
117
+ return appData ? path.join(appData, "Claude", "claude_desktop_config.json") : null;
118
+ }
119
+ if (platform === "darwin") {
120
+ return path.join(homeDirectory, "Library", "Application Support", "Claude", "claude_desktop_config.json");
121
+ }
122
+ return null;
123
+ }
124
+ async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
125
+ if (genericMcpResult.status !== "pass") {
126
+ return {
127
+ client: "Claude Desktop",
128
+ status: "skipped",
129
+ summary: "No valid MCP package config is available for Claude Desktop.",
130
+ details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
131
+ };
132
+ }
133
+ const configPath = getClaudeDesktopConfigPath(environment);
134
+ if (!configPath) {
135
+ return {
136
+ client: "Claude Desktop",
137
+ status: "warn",
138
+ summary: "Claude Desktop config path could not be resolved on this platform.",
139
+ details: ["Claude Desktop local config detection currently supports Windows and macOS."]
140
+ };
141
+ }
142
+ if (!(await fileExists(configPath))) {
143
+ const configDirectory = path.dirname(configPath);
144
+ return {
145
+ client: "Claude Desktop",
146
+ status: await directoryExists(configDirectory) ? "pass" : "warn",
147
+ summary: await directoryExists(configDirectory)
148
+ ? "Claude Desktop directory exists and a config file can be created."
149
+ : "Claude Desktop was not detected on this machine.",
150
+ details: [
151
+ configPath,
152
+ "Claude Desktop can add stdio MCP servers through `claude_desktop_config.json` when the app is installed."
153
+ ]
154
+ };
155
+ }
156
+ try {
157
+ const parsed = JSON.parse(await readFile(configPath, "utf8"));
158
+ const servers = parsed.mcpServers;
159
+ if (servers !== undefined && (typeof servers !== "object" ||
160
+ servers === null ||
161
+ Array.isArray(servers))) {
162
+ return {
163
+ client: "Claude Desktop",
164
+ status: "fail",
165
+ summary: "Claude Desktop config has an invalid `mcpServers` shape.",
166
+ details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
167
+ };
168
+ }
169
+ const packageServerNames = await readMcpServerNames(targetPath);
170
+ const existingServerNames = typeof servers === "object" && servers !== null
171
+ ? Object.keys(servers)
172
+ : [];
173
+ const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
174
+ if (duplicateServerNames.length > 0) {
175
+ return {
176
+ client: "Claude Desktop",
177
+ status: "warn",
178
+ summary: "Claude Desktop already has MCP server names from this package.",
179
+ details: [
180
+ configPath,
181
+ ...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
182
+ ]
183
+ };
184
+ }
185
+ return {
186
+ client: "Claude Desktop",
187
+ status: "pass",
188
+ summary: "Claude Desktop config is valid and this MCP package can be added.",
189
+ details: [
190
+ configPath,
191
+ `Source package: ${path.resolve(targetPath)}`
192
+ ]
193
+ };
194
+ }
195
+ catch {
196
+ return {
197
+ client: "Claude Desktop",
198
+ status: "fail",
199
+ summary: "Claude Desktop config is not valid JSON.",
200
+ details: [configPath, "Repair the local Claude Desktop config before adding new MCP servers."]
201
+ };
202
+ }
203
+ }
204
+ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
86
205
  const rootPath = path.resolve(targetPath);
87
206
  const genericMcpResult = await checkGenericMcp(rootPath);
207
+ const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
88
208
  const codexResult = await validatePlugin(rootPath);
89
209
  const codexStatus = statusFromCheckResult(codexResult);
90
210
  const codexCompatibility = !await hasCodexManifest(rootPath)
@@ -106,12 +226,7 @@ export async function buildCompatibilityMatrix(targetPath) {
106
226
  const results = [
107
227
  codexCompatibility,
108
228
  genericMcpResult,
109
- {
110
- client: "Claude Desktop",
111
- status: "skipped",
112
- summary: "Client-specific package adapter is not implemented yet.",
113
- details: ["Planned adapter after generic MCP compatibility is stable."]
114
- },
229
+ claudeDesktopResult,
115
230
  {
116
231
  client: "Cursor",
117
232
  status: "skipped",
package/dist/run-cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
3
3
  import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
4
+ import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
4
5
  import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
5
6
  import { initPluginPackage } from "./core/init-plugin.js";
6
7
  import { runCheck } from "./index.js";
@@ -25,7 +26,7 @@ const defaultIo = {
25
26
  }
26
27
  };
27
28
  function printUsage(io) {
28
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--json] [--output <path>]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
29
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--output <path>] [--install-preview]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
29
30
  }
30
31
  function renderInstalledPlugins(plugins) {
31
32
  const lines = [
@@ -113,6 +114,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
113
114
  ? [maybePath, ...remainingArgs]
114
115
  : remainingArgs;
115
116
  const jsonOutput = compatFlags.includes("--json");
117
+ const installPreview = compatFlags.includes("--install-preview");
116
118
  const clientIndex = compatFlags.indexOf("--client");
117
119
  const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
118
120
  const outputIndex = compatFlags.indexOf("--output");
@@ -125,7 +127,31 @@ export async function runCli(args, io = defaultIo, options = {}) {
125
127
  io.writeStderr("Missing path after --output.");
126
128
  return 2;
127
129
  }
128
- let matrix = await buildCompatibilityMatrix(targetPath);
130
+ if (installPreview && clientFilter?.toLowerCase() !== "claude-desktop") {
131
+ io.writeStderr("--install-preview requires --client claude-desktop.");
132
+ return 2;
133
+ }
134
+ if (installPreview) {
135
+ try {
136
+ const preview = await buildClaudeDesktopInstallPreview(targetPath, {
137
+ env: terminalContext.env
138
+ });
139
+ const report = renderClaudeDesktopInstallPreview(preview);
140
+ if (outputPath) {
141
+ await writeFile(outputPath, report, "utf8");
142
+ }
143
+ io.writeStdout(report);
144
+ return 0;
145
+ }
146
+ catch (error) {
147
+ const message = error instanceof Error ? error.message : "Unknown install preview error.";
148
+ io.writeStderr(message);
149
+ return 1;
150
+ }
151
+ }
152
+ let matrix = await buildCompatibilityMatrix(targetPath, {
153
+ env: terminalContext.env
154
+ });
129
155
  if (clientFilter) {
130
156
  const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
131
157
  if (!filteredMatrix) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",