codex-plugin-doctor 1.64.0 → 1.66.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.
@@ -0,0 +1,52 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, lstat, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { isBlockedOfflinePath, MAX_TOOL_FILE_BYTES } from "./mcp-tool-file.js";
5
+ // Serializes an enumerated catalog as one complete tools/list response that doctor tools-file and tools-diff accept.
6
+ export function serializeMcpToolsListResponse(tools, cache) {
7
+ return `${JSON.stringify({
8
+ jsonrpc: "2.0",
9
+ id: "tools-list",
10
+ result: { resultType: "complete", tools, ttlMs: cache.ttlMs, cacheScope: cache.cacheScope }
11
+ }, null, 2)}\n`;
12
+ }
13
+ export async function saveMcpToolsListResponse(filePath, tools, cache) {
14
+ if (isBlockedOfflinePath(filePath))
15
+ return { kind: "path-blocked" };
16
+ let content;
17
+ let bytes;
18
+ try {
19
+ content = serializeMcpToolsListResponse(tools, cache);
20
+ bytes = Buffer.byteLength(content);
21
+ }
22
+ catch {
23
+ return { kind: "write-failed" };
24
+ }
25
+ if (bytes > MAX_TOOL_FILE_BYTES)
26
+ return { kind: "too-large" };
27
+ let existingMode = null;
28
+ try {
29
+ const existing = await lstat(filePath);
30
+ if (existing.isSymbolicLink() || !existing.isFile())
31
+ return { kind: "not-regular" };
32
+ existingMode = existing.mode & 0o777;
33
+ }
34
+ catch (error) {
35
+ if (error.code !== "ENOENT")
36
+ return { kind: "write-failed" };
37
+ }
38
+ // Write beside the target and rename so a failed write never leaves a partial response file.
39
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
40
+ try {
41
+ // Create with the replaced baseline's permission bits, which may protect private tool definitions.
42
+ await writeFile(tempPath, content, { encoding: "utf8", flag: "wx", mode: existingMode ?? 0o666 });
43
+ if (existingMode !== null)
44
+ await chmod(tempPath, existingMode);
45
+ await rename(tempPath, filePath);
46
+ }
47
+ catch {
48
+ await rm(tempPath, { force: true }).catch(() => undefined);
49
+ return { kind: "write-failed" };
50
+ }
51
+ return { kind: "saved", bytes, tools: tools.length };
52
+ }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { packageVersion } from "../version.js";
3
+ import { TOOL_CHANGE_REASONS } from "./mcp-tool-impact.js";
3
4
  import { ruleCatalog } from "../rules/rule-catalog.js";
4
5
  const runtimeCapabilityStatusSchema = {
5
6
  type: "string",
@@ -785,33 +786,37 @@ publicSchemaDefinitions.push({
785
786
  before: embeddedToolFileReportSchema,
786
787
  after: embeddedToolFileReportSchema,
787
788
  comparison: {
788
- type: "object", required: ["complete", "reason", "added", "removed", "changed", "unchanged"],
789
+ type: "object", required: ["complete", "reason", "added", "removed", "changed", "unchanged", "breaking", "unclassified"],
789
790
  properties: {
790
791
  complete: { type: "boolean" },
791
792
  reason: { enum: [null, "input-blocked", "input-incomplete", "invalid-definitions", "ambiguous-names"] },
792
793
  added: { type: ["integer", "null"], minimum: 0, maximum: 500 },
793
794
  removed: { type: ["integer", "null"], minimum: 0, maximum: 500 },
794
795
  changed: { type: ["integer", "null"], minimum: 0, maximum: 500 },
795
- unchanged: { type: ["integer", "null"], minimum: 0, maximum: 500 }
796
+ unchanged: { type: ["integer", "null"], minimum: 0, maximum: 500 },
797
+ breaking: { type: ["integer", "null"], minimum: 0, maximum: 1000 },
798
+ unclassified: { type: ["integer", "null"], minimum: 0, maximum: 1000 }
796
799
  }, additionalProperties: false
797
800
  },
798
801
  coverage: {
799
- type: "object", required: ["comparison", "schema", "compatibility", "serverCatalog", "toolExecution"],
802
+ type: "object", required: ["comparison", "schema", "compatibility", "serverCatalog", "toolExecution", "impactClassification"],
800
803
  properties: {
801
804
  comparison: { const: "structural-only" }, schema: { const: "root-shape-only" },
802
805
  compatibility: { const: "not-tested" }, serverCatalog: { const: "not-tested" },
803
- toolExecution: { const: "not-tested" }
806
+ toolExecution: { const: "not-tested" }, impactClassification: { const: "heuristic" }
804
807
  }, additionalProperties: false
805
808
  },
806
809
  changes: {
807
810
  type: "array", maxItems: 1000,
808
811
  items: {
809
- type: "object", required: ["kind", "beforeToolIndex", "afterToolIndex", "fields"],
812
+ type: "object", required: ["kind", "beforeToolIndex", "afterToolIndex", "fields", "impact", "reasons"],
810
813
  properties: {
811
814
  kind: { enum: ["added", "removed", "changed"] },
812
815
  beforeToolIndex: { type: ["integer", "null"], minimum: 1, maximum: 500 },
813
816
  afterToolIndex: { type: ["integer", "null"], minimum: 1, maximum: 500 },
814
- fields: { type: "array", maxItems: 6, uniqueItems: true, items: { enum: ["inputSchema", "outputSchema", "description", "title", "annotations", "other"] } }
817
+ fields: { type: "array", maxItems: 6, uniqueItems: true, items: { enum: ["inputSchema", "outputSchema", "description", "title", "annotations", "other"] } },
818
+ impact: { enum: ["breaking", "compatible", "unclassified"] },
819
+ reasons: { type: "array", maxItems: TOOL_CHANGE_REASONS.length, uniqueItems: true, items: { enum: [...TOOL_CHANGE_REASONS] } }
815
820
  }, additionalProperties: false
816
821
  }
817
822
  }
@@ -11,12 +11,15 @@ export function renderMcpToolDiffReport(report) {
11
11
  `Removed: ${comparison.removed ?? "not compared"}`,
12
12
  `Changed: ${comparison.changed ?? "not compared"}`,
13
13
  `Unchanged: ${comparison.unchanged ?? "not compared"}`,
14
+ `Breaking: ${comparison.breaking ?? "not compared"}`,
15
+ `Unclassified: ${comparison.unclassified ?? "not compared"}`,
14
16
  `Comparison coverage: ${report.coverage.comparison}`,
15
17
  `Schema coverage: ${report.coverage.schema}`,
16
18
  `Compatibility: ${report.coverage.compatibility}`,
17
19
  `Server catalog: ${report.coverage.serverCatalog}`,
18
20
  `Tool execution: ${report.coverage.toolExecution}`,
19
- "Only the supplied responses are compared. A change does not establish incompatibility; an unchanged result does not establish compatibility.",
21
+ `Impact classification: ${report.coverage.impactClassification}`,
22
+ "Only the supplied responses are compared. Impact labels are root-level schema heuristics: a compatible label or an unchanged result does not establish compatibility.",
20
23
  ...["before", "after"].flatMap((side) => {
21
24
  const source = report[side];
22
25
  const label = side === "before" ? "Before" : "After";
@@ -35,7 +38,8 @@ export function renderMcpToolDiffReport(report) {
35
38
  ...(change.beforeToolIndex === null ? [] : [`before tool ${change.beforeToolIndex}`]),
36
39
  ...(change.afterToolIndex === null ? [] : [`after tool ${change.afterToolIndex}`])
37
40
  ];
38
- return `${change.kind.toUpperCase()} [${locations.join(" -> ")}]${change.fields.length ? `: ${change.fields.join(", ")}` : ""}`;
41
+ const impact = `${change.impact}${change.reasons.length ? ` (${change.reasons.join(", ")})` : ""}`;
42
+ return `${change.kind.toUpperCase()} [${locations.join(" -> ")}]${change.fields.length ? `: ${change.fields.join(", ")}` : ""}\n Impact: ${impact}`;
39
43
  })
40
44
  ].join("\n");
41
45
  }
package/dist/run-cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { discoverMcpServer } from "./core/mcp-discovery.js";
2
- import { inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
2
+ import { captureMcpToolCatalog, inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
3
3
  import { inspectMcpToolFile } from "./core/mcp-tool-file.js";
4
4
  import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
5
5
  import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport } from "./core/corpus-quality-metrics.js";
@@ -20,6 +20,7 @@ export interface RunCliOptions {
20
20
  compareMcpToolFilesImpl?: typeof compareMcpToolFiles;
21
21
  inspectMcpToolFileImpl?: typeof inspectMcpToolFile;
22
22
  inspectMcpToolCatalogImpl?: typeof inspectMcpToolCatalog;
23
+ captureMcpToolCatalogImpl?: typeof captureMcpToolCatalog;
23
24
  discoverMcpServerImpl?: typeof discoverMcpServer;
24
25
  terminalContext?: CliTerminalContext;
25
26
  runCheckImpl?: typeof runCheck;
package/dist/run-cli.js CHANGED
@@ -22,8 +22,9 @@ import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
22
22
  import { buildSubmissionArchivePreflight, submissionArchiveExitCode } from "./core/submission-archive-preflight.js";
23
23
  import { buildSubmissionPreflight } from "./core/submission-preflight.js";
24
24
  import { discoverMcpServer } from "./core/mcp-discovery.js";
25
- import { inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
26
- import { inspectMcpToolFile } from "./core/mcp-tool-file.js";
25
+ import { captureMcpToolCatalog, inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
26
+ import { inspectMcpToolFile, isBlockedOfflinePath } from "./core/mcp-tool-file.js";
27
+ import { saveMcpToolsListResponse } from "./core/mcp-tool-response-file.js";
27
28
  import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
28
29
  import { renderMcpToolDiffReport } from "./reporting/render-mcp-tool-diff-report.js";
29
30
  import { renderMcpToolFileReport } from "./reporting/render-mcp-tool-file-report.js";
@@ -107,9 +108,9 @@ function writeExactStdout(io, message) {
107
108
  }
108
109
  class CliUsageError extends Error {
109
110
  }
110
- const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <path> --after <path> [--json]";
111
+ const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <path> --after <path> [--json] [--fail-on any|breaking]";
111
112
  const toolsFileUsage = "Usage: codex-plugin-doctor doctor tools-file <path> [--json]";
112
- const toolsUsage = "Usage: codex-plugin-doctor doctor tools <url> --allow-network [--allow-local-network] [--json]";
113
+ const toolsUsage = "Usage: codex-plugin-doctor doctor tools <url> --allow-network [--allow-local-network] [--json] [--save-response <path>]";
113
114
  const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json]";
114
115
  function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
115
116
  const index = flags.indexOf("--sandbox");
@@ -1209,18 +1210,28 @@ export async function runCli(args, io = defaultIo, options = {}) {
1209
1210
  if (command === "doctor") {
1210
1211
  if (maybePath === "tools-diff") {
1211
1212
  if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1212
- io.writeStdout(`${toolsDiffUsage}\nCompares two saved MCP tools/list responses offline. Structural changes only; compatibility is not tested.`);
1213
+ io.writeStdout(`${toolsDiffUsage}\nCompares two saved MCP tools/list responses offline. Changes are classified heuristically as breaking, compatible, or unclassified; compatibility is not tested.\n--fail-on breaking exits 1 only for breaking or unclassified changes (default: any change or input warning).`);
1213
1214
  return 0;
1214
1215
  }
1215
1216
  let beforePath;
1216
1217
  let afterPath;
1217
1218
  let json = false;
1219
+ let failOn;
1218
1220
  let invalid = false;
1219
1221
  for (let index = 0; index < remainingArgs.length; index += 1) {
1220
1222
  const flag = remainingArgs[index];
1221
1223
  if (flag === "--json" && !json) {
1222
1224
  json = true;
1223
1225
  }
1226
+ else if (flag === "--fail-on" && failOn === undefined) {
1227
+ const value = remainingArgs[index + 1];
1228
+ if (value !== "any" && value !== "breaking") {
1229
+ invalid = true;
1230
+ break;
1231
+ }
1232
+ failOn = value;
1233
+ index += 1;
1234
+ }
1224
1235
  else if ((flag === "--before" && beforePath === undefined) || (flag === "--after" && afterPath === undefined)) {
1225
1236
  const value = remainingArgs[index + 1];
1226
1237
  if (!value || value.startsWith("-")) {
@@ -1244,7 +1255,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
1244
1255
  }
1245
1256
  const report = await (options.compareMcpToolFilesImpl ?? compareMcpToolFiles)(beforePath, afterPath);
1246
1257
  io.writeStdout(json ? JSON.stringify(report, null, 2) : renderMcpToolDiffReport(report));
1247
- return report.status === "pass" ? 0 : report.status === "warn" ? 1 : 2;
1258
+ if (report.status !== "pass" && report.status !== "warn")
1259
+ return 2;
1260
+ if (failOn === "breaking") {
1261
+ return (report.comparison.breaking ?? 0) + (report.comparison.unclassified ?? 0) > 0 ? 1 : 0;
1262
+ }
1263
+ return report.status === "pass" ? 0 : 1;
1248
1264
  }
1249
1265
  if (maybePath === "tools-file") {
1250
1266
  if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
@@ -1265,7 +1281,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
1265
1281
  }
1266
1282
  if (maybePath === "tools") {
1267
1283
  if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1268
- io.writeStdout(`${toolsUsage}\nHTTP tool catalog structure only; tools are not executed.`);
1284
+ io.writeStdout(`${toolsUsage}\nHTTP tool catalog structure only; tools are not executed.\n--save-response writes a complete enumeration as one tools/list response for doctor tools-file and tools-diff; nothing is written when enumeration is incomplete.`);
1269
1285
  return 0;
1270
1286
  }
1271
1287
  const [url, ...flags] = remainingArgs;
@@ -1274,23 +1290,68 @@ export async function runCli(args, io = defaultIo, options = {}) {
1274
1290
  return 2;
1275
1291
  }
1276
1292
  const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
1277
- if (flags.some((flag) => !allowedFlags.has(flag)) || new Set(flags).size !== flags.length) {
1293
+ const switches = [];
1294
+ let savePath;
1295
+ let invalid = false;
1296
+ for (let index = 0; index < flags.length; index += 1) {
1297
+ const flag = flags[index];
1298
+ if (flag === "--save-response" && savePath === undefined) {
1299
+ const value = flags[index + 1];
1300
+ if (!value || value.startsWith("-")) {
1301
+ invalid = true;
1302
+ break;
1303
+ }
1304
+ savePath = value;
1305
+ index += 1;
1306
+ }
1307
+ else if (allowedFlags.has(flag) && !switches.includes(flag)) {
1308
+ switches.push(flag);
1309
+ }
1310
+ else {
1311
+ invalid = true;
1312
+ break;
1313
+ }
1314
+ }
1315
+ if (invalid) {
1278
1316
  io.writeStderr(`Invalid or duplicate tool catalog arguments. ${toolsUsage}`);
1279
1317
  return 2;
1280
1318
  }
1281
- if (!flags.includes("--allow-network")) {
1319
+ if (!switches.includes("--allow-network")) {
1282
1320
  io.writeStderr("doctor tools requires explicit --allow-network consent.");
1283
1321
  return 2;
1284
1322
  }
1285
- const report = await (options.inspectMcpToolCatalogImpl ?? inspectMcpToolCatalog)(url, {
1286
- allowNetwork: true,
1287
- allowLocalNetwork: flags.includes("--allow-local-network")
1288
- });
1289
- io.writeStdout(flags.includes("--json")
1323
+ if (savePath !== undefined && isBlockedOfflinePath(savePath)) {
1324
+ io.writeStderr("doctor tools --save-response requires a local file path.");
1325
+ return 2;
1326
+ }
1327
+ const catalogOptions = { allowNetwork: true, allowLocalNetwork: switches.includes("--allow-local-network") };
1328
+ const snapshot = savePath === undefined
1329
+ ? { report: await (options.inspectMcpToolCatalogImpl ?? inspectMcpToolCatalog)(url, catalogOptions), tools: null, cache: null }
1330
+ : await (options.captureMcpToolCatalogImpl ?? captureMcpToolCatalog)(url, catalogOptions);
1331
+ const report = snapshot.report;
1332
+ io.writeStdout(switches.includes("--json")
1290
1333
  ? JSON.stringify(report, null, 2)
1291
1334
  : renderMcpToolCatalogReport(report));
1292
- return report.status === "pass" ? 0
1335
+ const exitCode = report.status === "pass" ? 0
1293
1336
  : report.status === "incomplete" || report.status === "blocked" ? 2 : 1;
1337
+ if (savePath === undefined)
1338
+ return exitCode;
1339
+ if (snapshot.tools === null || snapshot.cache === null) {
1340
+ io.writeStderr("Tool catalog response was not saved because enumeration did not complete.");
1341
+ return 2;
1342
+ }
1343
+ const saved = await saveMcpToolsListResponse(savePath, snapshot.tools, snapshot.cache);
1344
+ if (saved.kind !== "saved") {
1345
+ io.writeStderr({
1346
+ "path-blocked": "Tool catalog response was not saved because the path is not a local file path.",
1347
+ "not-regular": "Tool catalog response was not saved because the existing target is not a regular file.",
1348
+ "too-large": "Tool catalog response was not saved because it exceeds the one MiB tools-file limit.",
1349
+ "write-failed": "Tool catalog response could not be written."
1350
+ }[saved.kind]);
1351
+ return 2;
1352
+ }
1353
+ io.writeStderr(`Saved a complete tools/list response with ${saved.tools} tools (${saved.bytes} bytes).`);
1354
+ return exitCode;
1294
1355
  }
1295
1356
  if (maybePath === "discover") {
1296
1357
  if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
@@ -1,94 +1,94 @@
1
- # Examples
2
-
3
- This folder contains manual example packs for local testing. Unlike `tests/fixtures`, these examples are meant for humans to run directly against the CLI.
4
-
5
- ## Example Packs
6
-
7
- Use the smallest example that matches what you need to prove:
8
-
9
- - `codex-doctor-starter` for static manifest and skill validation.
10
- - `codex-doctor-runtime` for full MCP runtime probing and compatibility scoring.
11
- - `codex-doctor-risky` for security finding output.
12
- - `codex-doctor-generic-mcp` for MCP-only packages without a Codex plugin manifest.
13
-
14
- ### `codex-doctor-starter`
15
-
16
- Minimal valid Codex plugin package with one skill and no runtime MCP server.
17
-
18
- Expected result:
19
-
20
- - static validation passes
21
- - no runtime probing needed
22
-
23
- Command:
24
-
25
- ```bash
26
- codex-plugin-doctor check examples/codex-doctor-starter
27
- ```
28
-
29
- ### `codex-doctor-runtime`
30
-
31
- Valid Codex plugin package with:
32
-
33
- - skill metadata
34
- - `.mcp.json`
35
- - mock MCP stdio server
36
- - `tools/list`
37
- - `tools/call`
38
- - `resources/list`
39
- - `resources/read`
40
- - `resources/templates/list`
41
- - `prompts/list`
42
- - `prompts/get`
43
-
44
- Expected result:
45
-
46
- - static validation passes
47
- - runtime validation passes
48
- - runtime scorecard shows all supported runtime capabilities as `pass`
49
-
50
- Command:
51
-
52
- ```bash
53
- codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
54
- ```
55
-
56
- ### `codex-doctor-risky`
57
-
58
- Intentionally flawed package for showing failure output.
59
-
60
- Expected result:
61
-
62
- - security finding for hard-coded secret
63
-
64
- Command:
65
-
66
- ```bash
67
- codex-plugin-doctor check examples/codex-doctor-risky --ascii
68
- ```
69
-
70
- ### `codex-doctor-generic-mcp`
71
-
72
- Generic MCP package with a `.mcp.json` file and no Codex plugin manifest.
73
-
74
- Expected result:
75
-
76
- - `codex-plugin-doctor mcp` passes generic MCP static health checks
77
- - `codex-plugin-doctor check` still reports the missing Codex plugin manifest
78
-
79
- Command:
80
-
81
- ```bash
82
- codex-plugin-doctor mcp examples/codex-doctor-generic-mcp
83
- ```
84
-
85
- ## Suggested Local Flow
86
-
87
- ```bash
88
- npm install
89
- npm run build
90
- npm link
91
- codex-plugin-doctor check examples/codex-doctor-starter
92
- codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
93
- codex-plugin-doctor check examples/codex-doctor-risky --ascii
94
- ```
1
+ # Examples
2
+
3
+ This folder contains manual example packs for local testing. Unlike `tests/fixtures`, these examples are meant for humans to run directly against the CLI.
4
+
5
+ ## Example Packs
6
+
7
+ Use the smallest example that matches what you need to prove:
8
+
9
+ - `codex-doctor-starter` for static manifest and skill validation.
10
+ - `codex-doctor-runtime` for full MCP runtime probing and compatibility scoring.
11
+ - `codex-doctor-risky` for security finding output.
12
+ - `codex-doctor-generic-mcp` for MCP-only packages without a Codex plugin manifest.
13
+
14
+ ### `codex-doctor-starter`
15
+
16
+ Minimal valid Codex plugin package with one skill and no runtime MCP server.
17
+
18
+ Expected result:
19
+
20
+ - static validation passes
21
+ - no runtime probing needed
22
+
23
+ Command:
24
+
25
+ ```bash
26
+ codex-plugin-doctor check examples/codex-doctor-starter
27
+ ```
28
+
29
+ ### `codex-doctor-runtime`
30
+
31
+ Valid Codex plugin package with:
32
+
33
+ - skill metadata
34
+ - `.mcp.json`
35
+ - mock MCP stdio server
36
+ - `tools/list`
37
+ - `tools/call`
38
+ - `resources/list`
39
+ - `resources/read`
40
+ - `resources/templates/list`
41
+ - `prompts/list`
42
+ - `prompts/get`
43
+
44
+ Expected result:
45
+
46
+ - static validation passes
47
+ - runtime validation passes
48
+ - runtime scorecard shows all supported runtime capabilities as `pass`
49
+
50
+ Command:
51
+
52
+ ```bash
53
+ codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
54
+ ```
55
+
56
+ ### `codex-doctor-risky`
57
+
58
+ Intentionally flawed package for showing failure output.
59
+
60
+ Expected result:
61
+
62
+ - security finding for hard-coded secret
63
+
64
+ Command:
65
+
66
+ ```bash
67
+ codex-plugin-doctor check examples/codex-doctor-risky --ascii
68
+ ```
69
+
70
+ ### `codex-doctor-generic-mcp`
71
+
72
+ Generic MCP package with a `.mcp.json` file and no Codex plugin manifest.
73
+
74
+ Expected result:
75
+
76
+ - `codex-plugin-doctor mcp` passes generic MCP static health checks
77
+ - `codex-plugin-doctor check` still reports the missing Codex plugin manifest
78
+
79
+ Command:
80
+
81
+ ```bash
82
+ codex-plugin-doctor mcp examples/codex-doctor-generic-mcp
83
+ ```
84
+
85
+ ## Suggested Local Flow
86
+
87
+ ```bash
88
+ npm install
89
+ npm run build
90
+ npm link
91
+ codex-plugin-doctor check examples/codex-doctor-starter
92
+ codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
93
+ codex-plugin-doctor check examples/codex-doctor-risky --ascii
94
+ ```
@@ -1,10 +1,10 @@
1
- {
2
- "mcpServers": {
3
- "genericRuntime": {
4
- "command": "node",
5
- "args": [
6
- "server.js"
7
- ]
8
- }
9
- }
10
- }
1
+ {
2
+ "mcpServers": {
3
+ "genericRuntime": {
4
+ "command": "node",
5
+ "args": [
6
+ "server.js"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -1 +1 @@
1
- process.stdin.resume();
1
+ process.stdin.resume();
@@ -1,7 +1,7 @@
1
- {
2
- "name": "codex-doctor-risky",
3
- "version": "1.0.0",
4
- "description": "Intentionally risky sample package for showing Codex Doctor failure output.",
5
- "mcpServers": "./.mcp.json"
6
- }
7
-
1
+ {
2
+ "name": "codex-doctor-risky",
3
+ "version": "1.0.0",
4
+ "description": "Intentionally risky sample package for showing Codex Doctor failure output.",
5
+ "mcpServers": "./.mcp.json"
6
+ }
7
+
@@ -1,12 +1,12 @@
1
- {
2
- "mcpServers": {
3
- "dangerServer": {
4
- "command": "node",
5
- "args": ["./mock-server.js"],
6
- "env": {
7
- "OPENAI_API_KEY": "sk-live-example-secret-value"
8
- }
9
- }
10
- }
11
- }
12
-
1
+ {
2
+ "mcpServers": {
3
+ "dangerServer": {
4
+ "command": "node",
5
+ "args": ["./mock-server.js"],
6
+ "env": {
7
+ "OPENAI_API_KEY": "sk-live-example-secret-value"
8
+ }
9
+ }
10
+ }
11
+ }
12
+
@@ -1 +1 @@
1
- process.stdin.resume();
1
+ process.stdin.resume();
@@ -1,8 +1,8 @@
1
- {
2
- "name": "codex-doctor-runtime",
3
- "version": "1.0.0",
4
- "description": "Runtime-complete Codex Doctor sample plugin with MCP validation coverage.",
5
- "skills": "./skills/",
6
- "mcpServers": "./.mcp.json"
7
- }
8
-
1
+ {
2
+ "name": "codex-doctor-runtime",
3
+ "version": "1.0.0",
4
+ "description": "Runtime-complete Codex Doctor sample plugin with MCP validation coverage.",
5
+ "skills": "./skills/",
6
+ "mcpServers": "./.mcp.json"
7
+ }
8
+
@@ -1,9 +1,9 @@
1
- {
2
- "mcpServers": {
3
- "doctorRuntime": {
4
- "command": "node",
5
- "args": ["./mock-server.js"]
6
- }
7
- }
8
- }
9
-
1
+ {
2
+ "mcpServers": {
3
+ "doctorRuntime": {
4
+ "command": "node",
5
+ "args": ["./mock-server.js"]
6
+ }
7
+ }
8
+ }
9
+