codex-plugin-doctor 0.3.0 → 0.4.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/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Codex Plugin Doctor
2
2
 
3
3
  [![CI](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml/badge.svg)](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/codex-plugin-doctor.svg)](https://www.npmjs.com/package/codex-plugin-doctor)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
+ [![GitHub release](https://img.shields.io/github/v/release/Esquetta/CodexPluginDoctor)](https://github.com/Esquetta/CodexPluginDoctor/releases)
4
7
 
5
8
  Codex Plugin Doctor is a local CLI validator for Codex plugin packages, skills, and MCP server bundles.
6
9
 
@@ -72,6 +75,7 @@ Global install from npm:
72
75
  ```bash
73
76
  npm install -g codex-plugin-doctor
74
77
  codex-plugin-doctor --version
78
+ codex-plugin-doctor self-test
75
79
  codex-plugin-doctor check path/to/plugin-package
76
80
  ```
77
81
 
@@ -156,14 +160,17 @@ Run these from a Codex plugin package root:
156
160
 
157
161
  ```bash
158
162
  codex-plugin-doctor --version
163
+ codex-plugin-doctor self-test
159
164
  codex-plugin-doctor init my-plugin
160
165
  codex-plugin-doctor compat .
161
166
  codex-plugin-doctor compat . --client codex
162
167
  codex-plugin-doctor compat . --client generic-mcp
163
168
  codex-plugin-doctor compat . --client claude-desktop
164
169
  codex-plugin-doctor compat . --client claude-desktop --install-preview
170
+ codex-plugin-doctor compat . --client claude-desktop --apply --backup
165
171
  codex-plugin-doctor compat . --client cursor
166
172
  codex-plugin-doctor compat . --client cursor --install-preview
173
+ codex-plugin-doctor compat . --client cursor --apply --backup
167
174
  codex-plugin-doctor compat . --scorecard
168
175
  codex-plugin-doctor compat . --json
169
176
  codex-plugin-doctor compat . --json --output compatibility.json
@@ -179,9 +186,11 @@ codex-plugin-doctor check . --config .codex-doctor.json
179
186
  codex-plugin-doctor check . --json --runtime --verbose-runtime
180
187
  ```
181
188
 
182
- `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.
189
+ `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
183
190
 
184
- `compat --client cursor` checks whether the MCP package can be added to Cursor. It prefers a project-level `.cursor/mcp.json` when one already exists in the target package, then falls back to the global `~/.cursor/mcp.json` path. A valid existing config returns `PASS`, a missing Cursor config returns `WARN`, malformed JSON returns `FAIL`, and duplicate MCP server names return `WARN`. Add `--install-preview` to print the JSON snippet that should be merged into Cursor's `mcp.json`; it does not modify files.
191
+ `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. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
192
+
193
+ `compat --client cursor` checks whether the MCP package can be added to Cursor. It prefers a project-level `.cursor/mcp.json` when one already exists in the target package, then falls back to the global `~/.cursor/mcp.json` path. A valid existing config returns `PASS`, a missing Cursor config returns `WARN`, malformed JSON returns `FAIL`, and duplicate MCP server names return `WARN`. Add `--install-preview` to print the JSON snippet that should be merged into Cursor's `mcp.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
185
194
 
186
195
  `compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
187
196
 
@@ -269,6 +278,9 @@ This runs tests, builds the TypeScript output, and performs `npm pack --dry-run`
269
278
  Related docs:
270
279
 
271
280
  - [Changelog](./CHANGELOG.md)
281
+ - [Contributing](./CONTRIBUTING.md)
282
+ - [Security Policy](./SECURITY.md)
283
+ - [Code of Conduct](./CODE_OF_CONDUCT.md)
272
284
  - [NPM Release Checklist](./docs/engineering/npm-release-checklist.md)
273
285
  - [Release Candidate Workflow](./docs/engineering/release-candidate-workflow.md)
274
286
  - [v0.1.0 Release Notes](./docs/engineering/v0.1.0-final-release-notes.md)
@@ -0,0 +1,15 @@
1
+ export interface McpInstallPreviewLike {
2
+ targetPath: string;
3
+ configPath: string;
4
+ snippet: {
5
+ mcpServers: Record<string, unknown>;
6
+ };
7
+ }
8
+ export interface ApplyInstallResult {
9
+ client: string;
10
+ configPath: string;
11
+ backupPath: string | null;
12
+ appliedServers: string[];
13
+ }
14
+ export declare function applyInstallPreview(client: string, preview: McpInstallPreviewLike): Promise<ApplyInstallResult>;
15
+ export declare function renderApplyInstallResult(result: ApplyInstallResult): string;
@@ -0,0 +1,71 @@
1
+ import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readJsonFile } from "../core/read-json-file.js";
4
+ async function fileExists(targetPath) {
5
+ try {
6
+ const details = await stat(targetPath);
7
+ return details.isFile();
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function buildBackupPath(configPath) {
17
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
18
+ return `${configPath}.${timestamp}.bak`;
19
+ }
20
+ export async function applyInstallPreview(client, preview) {
21
+ await mkdir(path.dirname(preview.configPath), { recursive: true });
22
+ const configExists = await fileExists(preview.configPath);
23
+ const currentConfig = configExists
24
+ ? await readJsonFile(preview.configPath)
25
+ : {};
26
+ if (!isRecord(currentConfig)) {
27
+ throw new Error(`${client} MCP config must be a JSON object.`);
28
+ }
29
+ const currentServers = currentConfig.mcpServers;
30
+ if (currentServers !== undefined && !isRecord(currentServers)) {
31
+ throw new Error(`${client} MCP config has an invalid \`mcpServers\` shape.`);
32
+ }
33
+ const existingServers = currentServers ?? {};
34
+ const incomingServers = preview.snippet.mcpServers;
35
+ const duplicateServers = Object.keys(incomingServers).filter((serverName) => Object.prototype.hasOwnProperty.call(existingServers, serverName));
36
+ if (duplicateServers.length > 0) {
37
+ throw new Error(`Refusing to overwrite existing MCP server names: ${duplicateServers.join(", ")}`);
38
+ }
39
+ const backupPath = configExists ? buildBackupPath(preview.configPath) : null;
40
+ if (backupPath) {
41
+ await copyFile(preview.configPath, backupPath);
42
+ }
43
+ const nextConfig = {
44
+ ...currentConfig,
45
+ mcpServers: {
46
+ ...existingServers,
47
+ ...incomingServers
48
+ }
49
+ };
50
+ await writeFile(preview.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
51
+ return {
52
+ client,
53
+ configPath: preview.configPath,
54
+ backupPath,
55
+ appliedServers: Object.keys(incomingServers)
56
+ };
57
+ }
58
+ export function renderApplyInstallResult(result) {
59
+ const lines = [
60
+ `Applied ${result.client} MCP config`,
61
+ "==============================",
62
+ `Config: ${result.configPath}`,
63
+ `Backup: ${result.backupPath ?? "No existing config file was present."}`,
64
+ "",
65
+ "Applied servers:"
66
+ ];
67
+ for (const serverName of result.appliedServers) {
68
+ lines.push(`- ${serverName}`);
69
+ }
70
+ return lines.join("\n");
71
+ }
@@ -1,7 +1,8 @@
1
- import { readFile, stat } from "node:fs/promises";
1
+ import { stat } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { validatePlugin } from "../core/validate-plugin.js";
5
+ import { readJsonFile } from "../core/read-json-file.js";
5
6
  async function fileExists(targetPath) {
6
7
  try {
7
8
  const details = await stat(targetPath);
@@ -40,7 +41,7 @@ export async function readMcpConfigPath(targetPath) {
40
41
  return null;
41
42
  }
42
43
  try {
43
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
44
+ const manifest = await readJsonFile(manifestPath);
44
45
  return typeof manifest.mcpServers === "string"
45
46
  ? path.resolve(rootPath, manifest.mcpServers)
46
47
  : null;
@@ -63,7 +64,7 @@ async function checkGenericMcp(targetPath) {
63
64
  };
64
65
  }
65
66
  try {
66
- const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
67
+ const parsed = await readJsonFile(mcpConfigPath);
67
68
  const servers = parsed.mcpServers;
68
69
  if (typeof servers !== "object" ||
69
70
  servers === null ||
@@ -98,7 +99,7 @@ async function readMcpServerNames(targetPath) {
98
99
  return [];
99
100
  }
100
101
  try {
101
- const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
102
+ const parsed = await readJsonFile(mcpConfigPath);
102
103
  const servers = parsed.mcpServers;
103
104
  return typeof servers === "object" && servers !== null && !Array.isArray(servers)
104
105
  ? Object.keys(servers)
@@ -166,7 +167,7 @@ async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}
166
167
  };
167
168
  }
168
169
  try {
169
- const parsed = JSON.parse(await readFile(configPath, "utf8"));
170
+ const parsed = await readJsonFile(configPath);
170
171
  const servers = parsed.mcpServers;
171
172
  if (servers !== undefined && (typeof servers !== "object" ||
172
173
  servers === null ||
@@ -238,7 +239,7 @@ async function checkCursor(targetPath, genericMcpResult, environment = {}) {
238
239
  };
239
240
  }
240
241
  try {
241
- const parsed = JSON.parse(await readFile(configPath, "utf8"));
242
+ const parsed = await readJsonFile(configPath);
242
243
  const servers = parsed.mcpServers;
243
244
  if (servers !== undefined && (typeof servers !== "object" ||
244
245
  servers === null ||
@@ -0,0 +1,2 @@
1
+ export declare function parseJsonText<T>(text: string): T;
2
+ export declare function readJsonFile<T>(filePath: string): Promise<T>;
@@ -0,0 +1,8 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export function parseJsonText(text) {
3
+ const normalizedText = text.startsWith("\uFEFF") ? text.slice(1) : text;
4
+ return JSON.parse(normalizedText);
5
+ }
6
+ export async function readJsonFile(filePath) {
7
+ return parseJsonText(await readFile(filePath, "utf8"));
8
+ }
package/dist/run-cli.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
3
5
  import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
6
+ import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
4
7
  import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
5
8
  import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
6
9
  import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
@@ -28,7 +31,7 @@ const defaultIo = {
28
31
  }
29
32
  };
30
33
  function printUsage(io) {
31
- 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] [--scorecard] [--output <path>] [--install-preview]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
34
+ 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] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
32
35
  }
33
36
  function renderInstalledPlugins(plugins) {
34
37
  const lines = [
@@ -66,6 +69,22 @@ function filterCompatibilityMatrix(matrix, clientFilter) {
66
69
  results: matrix.results.filter((result) => result.client === client)
67
70
  };
68
71
  }
72
+ function resolveBundledSelfTestTarget() {
73
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "examples", "codex-doctor-runtime");
74
+ }
75
+ function renderSelfTestReport(targetPath, validationStatus, findingsCount, compatibilityMatrix) {
76
+ return [
77
+ "Codex Plugin Doctor Self-Test",
78
+ "=============================",
79
+ `Version: ${packageVersion}`,
80
+ `Sample: ${targetPath}`,
81
+ `Validation: ${validationStatus.toUpperCase()}`,
82
+ "Runtime probes: enabled",
83
+ `Findings: ${findingsCount}`,
84
+ "",
85
+ renderCompatibilityScorecard(compatibilityMatrix)
86
+ ].join("\n");
87
+ }
69
88
  export async function runCli(args, io = defaultIo, options = {}) {
70
89
  const [command, maybePath, ...remainingArgs] = args;
71
90
  if (command === "--version" || command === "-v" || command === "version") {
@@ -97,6 +116,16 @@ export async function runCli(args, io = defaultIo, options = {}) {
97
116
  io.writeStdout(renderRuleExplanation(rule));
98
117
  return 0;
99
118
  }
119
+ if (command === "self-test" || command === "demo") {
120
+ const targetPath = resolveBundledSelfTestTarget();
121
+ const runCheckImpl = options.runCheckImpl ?? runCheck;
122
+ const result = applyDoctorConfig(await runCheckImpl(targetPath, { runtime: true }), await loadDoctorConfig(targetPath));
123
+ const compatibilityMatrix = await buildCompatibilityMatrix(targetPath, {
124
+ env: terminalContext.env
125
+ });
126
+ io.writeStdout(renderSelfTestReport(targetPath, result.status, result.findings.length, compatibilityMatrix));
127
+ return result.exitCode === 1 || matrixExitCode(compatibilityMatrix) === 1 ? 1 : 0;
128
+ }
100
129
  if (command === "init") {
101
130
  const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
102
131
  const result = await initPluginPackage(targetPath);
@@ -118,6 +147,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
118
147
  const jsonOutput = compatFlags.includes("--json");
119
148
  const scorecardOutput = compatFlags.includes("--scorecard");
120
149
  const installPreview = compatFlags.includes("--install-preview");
150
+ const applyInstall = compatFlags.includes("--apply");
151
+ const backupInstall = compatFlags.includes("--backup");
121
152
  const clientIndex = compatFlags.indexOf("--client");
122
153
  const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
123
154
  const outputIndex = compatFlags.indexOf("--output");
@@ -130,21 +161,34 @@ export async function runCli(args, io = defaultIo, options = {}) {
130
161
  io.writeStderr("Missing path after --output.");
131
162
  return 2;
132
163
  }
133
- if (installPreview &&
164
+ if ((installPreview || applyInstall) &&
134
165
  clientFilter?.toLowerCase() !== "claude-desktop" &&
135
166
  clientFilter?.toLowerCase() !== "cursor") {
136
- io.writeStderr("--install-preview requires --client claude-desktop or --client cursor.");
167
+ io.writeStderr("--install-preview and --apply require --client claude-desktop or --client cursor.");
168
+ return 2;
169
+ }
170
+ if (installPreview && applyInstall) {
171
+ io.writeStderr("Use either --install-preview or --apply, not both.");
172
+ return 2;
173
+ }
174
+ if (applyInstall && !backupInstall) {
175
+ io.writeStderr("--apply requires --backup.");
137
176
  return 2;
138
177
  }
139
- if (installPreview) {
178
+ if (installPreview || applyInstall) {
140
179
  try {
141
- const report = clientFilter?.toLowerCase() === "cursor"
142
- ? renderCursorInstallPreview(await buildCursorInstallPreview(targetPath, {
180
+ const preview = clientFilter?.toLowerCase() === "cursor"
181
+ ? await buildCursorInstallPreview(targetPath, {
143
182
  env: terminalContext.env
144
- }))
145
- : renderClaudeDesktopInstallPreview(await buildClaudeDesktopInstallPreview(targetPath, {
183
+ })
184
+ : await buildClaudeDesktopInstallPreview(targetPath, {
146
185
  env: terminalContext.env
147
- }));
186
+ });
187
+ const report = applyInstall
188
+ ? renderApplyInstallResult(await applyInstallPreview(clientFilter?.toLowerCase() === "cursor" ? "Cursor" : "Claude Desktop", preview))
189
+ : clientFilter?.toLowerCase() === "cursor"
190
+ ? renderCursorInstallPreview(preview)
191
+ : renderClaudeDesktopInstallPreview(preview);
148
192
  if (outputPath) {
149
193
  await writeFile(outputPath, report, "utf8");
150
194
  }
@@ -0,0 +1,73 @@
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
+ ### `codex-doctor-starter`
8
+
9
+ Minimal valid Codex plugin package with one skill and no runtime MCP server.
10
+
11
+ Expected result:
12
+
13
+ - static validation passes
14
+ - no runtime probing needed
15
+
16
+ Command:
17
+
18
+ ```bash
19
+ codex-plugin-doctor check examples/codex-doctor-starter
20
+ ```
21
+
22
+ ### `codex-doctor-runtime`
23
+
24
+ Valid Codex plugin package with:
25
+
26
+ - skill metadata
27
+ - `.mcp.json`
28
+ - mock MCP stdio server
29
+ - `tools/list`
30
+ - `tools/call`
31
+ - `resources/list`
32
+ - `resources/read`
33
+ - `resources/templates/list`
34
+ - `prompts/list`
35
+ - `prompts/get`
36
+
37
+ Expected result:
38
+
39
+ - static validation passes
40
+ - runtime validation passes
41
+ - runtime scorecard shows all supported runtime capabilities as `pass`
42
+
43
+ Command:
44
+
45
+ ```bash
46
+ codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
47
+ ```
48
+
49
+ ### `codex-doctor-risky`
50
+
51
+ Intentionally flawed package for showing failure output.
52
+
53
+ Expected result:
54
+
55
+ - security finding for hard-coded secret
56
+
57
+ Command:
58
+
59
+ ```bash
60
+ codex-plugin-doctor check examples/codex-doctor-risky --ascii
61
+ ```
62
+
63
+ ## Suggested Local Flow
64
+
65
+ ```bash
66
+ npm install
67
+ npm run build
68
+ npm link
69
+ codex-plugin-doctor check examples/codex-doctor-starter
70
+ codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
71
+ codex-plugin-doctor check examples/codex-doctor-risky --ascii
72
+ ```
73
+
@@ -0,0 +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
+
@@ -0,0 +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
+
@@ -0,0 +1 @@
1
+ process.stdin.resume();
@@ -0,0 +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
+
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "doctorRuntime": {
4
+ "command": "node",
5
+ "args": ["./mock-server.js"]
6
+ }
7
+ }
8
+ }
9
+
@@ -0,0 +1,237 @@
1
+ import readline from "node:readline";
2
+
3
+ const rl = readline.createInterface({
4
+ input: process.stdin,
5
+ crlfDelay: Infinity
6
+ });
7
+
8
+ rl.on("line", (line) => {
9
+ const message = JSON.parse(line);
10
+ const cursor = message.params?.cursor;
11
+
12
+ if (message.method === "initialize") {
13
+ process.stdout.write(
14
+ `${JSON.stringify({
15
+ jsonrpc: "2.0",
16
+ id: message.id,
17
+ result: {
18
+ protocolVersion: "2025-11-25",
19
+ capabilities: {
20
+ tools: {},
21
+ resources: {},
22
+ prompts: {}
23
+ },
24
+ serverInfo: {
25
+ name: "doctor-runtime-server",
26
+ version: "1.0.0"
27
+ }
28
+ }
29
+ })}\n`
30
+ );
31
+ return;
32
+ }
33
+
34
+ if (message.method === "tools/list") {
35
+ process.stdout.write(
36
+ `${JSON.stringify({
37
+ jsonrpc: "2.0",
38
+ id: message.id,
39
+ result:
40
+ cursor === "tools-page-2"
41
+ ? {
42
+ tools: [
43
+ {
44
+ name: "format_status",
45
+ description: "Return a formatted health status.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {},
49
+ required: []
50
+ }
51
+ }
52
+ ]
53
+ }
54
+ : {
55
+ tools: [
56
+ {
57
+ name: "ping",
58
+ description: "Return a healthcheck response.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {},
62
+ required: []
63
+ }
64
+ }
65
+ ],
66
+ nextCursor: "tools-page-2"
67
+ }
68
+ })}\n`
69
+ );
70
+ return;
71
+ }
72
+
73
+ if (message.method === "tools/call") {
74
+ process.stdout.write(
75
+ `${JSON.stringify({
76
+ jsonrpc: "2.0",
77
+ id: message.id,
78
+ result: {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: "doctor-runtime-ok"
83
+ }
84
+ ]
85
+ }
86
+ })}\n`
87
+ );
88
+ return;
89
+ }
90
+
91
+ if (message.method === "resources/list") {
92
+ process.stdout.write(
93
+ `${JSON.stringify({
94
+ jsonrpc: "2.0",
95
+ id: message.id,
96
+ result:
97
+ cursor === "resources-page-2"
98
+ ? {
99
+ resources: [
100
+ {
101
+ name: "workspace-license",
102
+ uri: "file:///workspace/LICENSE"
103
+ }
104
+ ]
105
+ }
106
+ : {
107
+ resources: [
108
+ {
109
+ name: "workspace-readme",
110
+ uri: "file:///workspace/README.md"
111
+ }
112
+ ],
113
+ nextCursor: "resources-page-2"
114
+ }
115
+ })}\n`
116
+ );
117
+ return;
118
+ }
119
+
120
+ if (message.method === "resources/read") {
121
+ process.stdout.write(
122
+ `${JSON.stringify({
123
+ jsonrpc: "2.0",
124
+ id: message.id,
125
+ result: {
126
+ contents: [
127
+ {
128
+ uri: "file:///workspace/README.md",
129
+ text: "# Workspace README"
130
+ }
131
+ ]
132
+ }
133
+ })}\n`
134
+ );
135
+ return;
136
+ }
137
+
138
+ if (message.method === "resources/templates/list") {
139
+ process.stdout.write(
140
+ `${JSON.stringify({
141
+ jsonrpc: "2.0",
142
+ id: message.id,
143
+ result:
144
+ cursor === "templates-page-2"
145
+ ? {
146
+ resourceTemplates: [
147
+ {
148
+ name: "log",
149
+ uriTemplate: "file:///workspace/logs/{name}.log"
150
+ }
151
+ ]
152
+ }
153
+ : {
154
+ resourceTemplates: [
155
+ {
156
+ name: "doc",
157
+ uriTemplate: "file:///workspace/docs/{name}.md"
158
+ }
159
+ ],
160
+ nextCursor: "templates-page-2"
161
+ }
162
+ })}\n`
163
+ );
164
+ return;
165
+ }
166
+
167
+ if (message.method === "prompts/list") {
168
+ process.stdout.write(
169
+ `${JSON.stringify({
170
+ jsonrpc: "2.0",
171
+ id: message.id,
172
+ result:
173
+ cursor === "prompts-page-2"
174
+ ? {
175
+ prompts: [
176
+ {
177
+ name: "summary",
178
+ description: "Summarize the current change."
179
+ }
180
+ ]
181
+ }
182
+ : {
183
+ prompts: [
184
+ {
185
+ name: "code_review",
186
+ description: "Review code for bugs.",
187
+ arguments: [
188
+ {
189
+ name: "diff",
190
+ required: true
191
+ }
192
+ ]
193
+ }
194
+ ],
195
+ nextCursor: "prompts-page-2"
196
+ }
197
+ })}\n`
198
+ );
199
+ return;
200
+ }
201
+
202
+ if (message.method === "prompts/get") {
203
+ if (message.params?.arguments?.diff !== "codex-plugin-doctor-probe") {
204
+ process.stdout.write(
205
+ `${JSON.stringify({
206
+ jsonrpc: "2.0",
207
+ id: message.id,
208
+ error: {
209
+ code: -32602,
210
+ message: "Missing required diff argument"
211
+ }
212
+ })}\n`
213
+ );
214
+ return;
215
+ }
216
+
217
+ process.stdout.write(
218
+ `${JSON.stringify({
219
+ jsonrpc: "2.0",
220
+ id: message.id,
221
+ result: {
222
+ description: "Prompt for code review",
223
+ messages: [
224
+ {
225
+ role: "user",
226
+ content: {
227
+ type: "text",
228
+ text: "Review this diff for bugs."
229
+ }
230
+ }
231
+ ]
232
+ }
233
+ })}\n`
234
+ );
235
+ }
236
+ });
237
+
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: context-check
3
+ description: Validate the current repository context before taking action.
4
+ ---
5
+
6
+ Use this skill when you need to confirm repository structure, expected files, and project readiness before deeper work begins.
7
+
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "codex-doctor-starter",
3
+ "version": "1.0.0",
4
+ "description": "Minimal Codex Doctor sample plugin for local validation.",
5
+ "skills": "./skills/"
6
+ }
7
+
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: repo-scan
3
+ description: Scan the repository and summarize the main components.
4
+ ---
5
+
6
+ Use this skill when you need a quick high-level map of the current repository.
7
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",
@@ -16,7 +16,8 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "files": [
19
- "dist"
19
+ "dist",
20
+ "examples"
20
21
  ],
21
22
  "scripts": {
22
23
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",