codex-plugin-doctor 1.69.1 → 1.70.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
@@ -127,7 +127,7 @@ compatibility.
127
127
  See [Offline MCP Tool Diff](docs/guides/offline-tool-diff.md) for matching rules,
128
128
  privacy, limits, and exit codes.
129
129
 
130
- ### HTTP MCP Tool Catalog
130
+ ### MCP Tool Catalog
131
131
 
132
132
  Inspect a modern HTTP MCP server's tool definitions without invoking them:
133
133
 
@@ -148,7 +148,20 @@ approved loopback endpoint; it is never printed or saved. The GitHub Action can
148
148
  live list with `tools-capture-url` (and `tools-capture-token-env`), and skip the
149
149
  plugin package check with `check: "false"` in repositories that only ship an MCP
150
150
  server.
151
- See [HTTP MCP Tool Catalog](docs/guides/mcp-tool-catalog.md).
151
+
152
+ Local stdio servers from a package's MCP configuration can be cataloged too.
153
+ `--runtime` is required because this starts package code; the server is stopped
154
+ after its tool list is read:
155
+
156
+ ```bash
157
+ codex-plugin-doctor doctor tools ./my-plugin --server my-server --runtime --save-response ./tools-current.json
158
+ ```
159
+
160
+ Doctor tries `server/discover` (`2026-07-28`) first and falls back to the
161
+ `initialize` handshake for older stdio servers, so the saved list works with
162
+ `tools-file` and `tools-diff` either way. `--sandbox docker` and
163
+ `--require-runtime-approval` work as they do for `check --runtime`.
164
+ See [MCP Tool Catalog](docs/guides/mcp-tool-catalog.md).
152
165
 
153
166
  ### Remote MCP Readiness
154
167
 
@@ -568,9 +581,9 @@ jobs:
568
581
  runs-on: ubuntu-latest
569
582
  steps:
570
583
  - uses: actions/checkout@v5
571
- - uses: Esquetta/CodexPluginDoctor@v1.69.1
584
+ - uses: Esquetta/CodexPluginDoctor@v1.70.0
572
585
  with:
573
- version: "1.69.1"
586
+ version: "1.70.0"
574
587
  path: .
575
588
  runtime: "true"
576
589
  policy: codex-publish
@@ -26,4 +26,23 @@ export interface McpDiscoveryReport {
26
26
  export declare function findMcpSseResponse(body: Buffer, requestId?: string): McpJsonObject | null;
27
27
  export declare function parseMcpResponse(response: BoundedHttpResponse, requestId?: string): McpJsonObject | null;
28
28
  export declare function discoverMcpCapabilities(response: BoundedHttpResponse, requestId?: string): McpJsonObject | null;
29
+ export declare function serializeMcpDiscoverRequest(requestId?: string): string;
30
+ export type McpDiscoverOutcome = {
31
+ kind: "discovered";
32
+ supportedVersions: string[];
33
+ capabilities: McpJsonObject;
34
+ } | {
35
+ kind: "version-not-listed";
36
+ supportedVersions: string[];
37
+ } | {
38
+ kind: "version-unsupported";
39
+ supportedVersions: string[];
40
+ } | {
41
+ kind: "result-invalid";
42
+ } | {
43
+ kind: "envelope-invalid";
44
+ } | {
45
+ kind: "other-error";
46
+ };
47
+ export declare function classifyMcpDiscoverMessage(message: McpJsonObject, requestId?: string): McpDiscoverOutcome;
29
48
  export declare function discoverMcpServer(rawUrl: string, options?: McpDiscoveryOptions): Promise<McpDiscoveryReport>;
@@ -102,6 +102,29 @@ export function discoverMcpCapabilities(response, requestId = REQUEST_ID) {
102
102
  return null;
103
103
  return result.supportedVersions.includes(REQUESTED_VERSION) ? capabilities : null;
104
104
  }
105
+ export function serializeMcpDiscoverRequest(requestId = REQUEST_ID) {
106
+ return JSON.stringify({ jsonrpc: "2.0", id: requestId, method: "server/discover", params: { _meta: {
107
+ "io.modelcontextprotocol/protocolVersion": REQUESTED_VERSION,
108
+ "io.modelcontextprotocol/clientInfo": { name: "Codex Plugin Doctor", version: packageVersion },
109
+ "io.modelcontextprotocol/clientCapabilities": {}
110
+ } } });
111
+ }
112
+ // Classifies a transport-neutral server/discover reply. Only a valid result or a valid
113
+ // UnsupportedProtocolVersionError identifies a modern server; other errors do not.
114
+ export function classifyMcpDiscoverMessage(message, requestId = REQUEST_ID) {
115
+ if (!hasMatchingResponseEnvelope(message, requestId))
116
+ return { kind: "envelope-invalid" };
117
+ if (Object.hasOwn(message, "error")) {
118
+ const versions = unsupportedVersions(message.error);
119
+ return versions === null ? { kind: "other-error" } : { kind: "version-unsupported", supportedVersions: versions };
120
+ }
121
+ const result = message.result;
122
+ if (!validDiscoverResult(result))
123
+ return { kind: "result-invalid" };
124
+ return result.supportedVersions.includes(REQUESTED_VERSION)
125
+ ? { kind: "discovered", supportedVersions: result.supportedVersions, capabilities: result.capabilities }
126
+ : { kind: "version-not-listed", supportedVersions: result.supportedVersions };
127
+ }
105
128
  function failureReport(id, message) {
106
129
  return report("failed", "fail", [finding(id, "fail", message, "The discovery response cannot safely establish modern MCP discovery support.", "Verify the endpoint returns a valid server/discover response for the requested protocol version.")]);
107
130
  }
@@ -116,11 +139,7 @@ export async function discoverMcpServer(rawUrl, options = {}) {
116
139
  return report("blocked", "skipped", [finding("plugin.discovery.url.invalid", "warn", "The MCP discovery endpoint is unsafe or unsupported.", "Unsafe endpoint URLs can bypass network controls or expose credentials.", "Use an absolute HTTP or HTTPS endpoint without credentials, query parameters, fragments, or IP literals.")]);
117
140
  }
118
141
  const request = options.request ?? requestBoundedHttp;
119
- const body = JSON.stringify({ jsonrpc: "2.0", id: REQUEST_ID, method: "server/discover", params: { _meta: {
120
- "io.modelcontextprotocol/protocolVersion": REQUESTED_VERSION,
121
- "io.modelcontextprotocol/clientInfo": { name: "Codex Plugin Doctor", version: packageVersion },
122
- "io.modelcontextprotocol/clientCapabilities": {}
123
- } } });
142
+ const body = serializeMcpDiscoverRequest();
124
143
  let response;
125
144
  try {
126
145
  response = await request(rawUrl, {
@@ -0,0 +1,31 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { RuntimeSandboxMode } from "../domain/types.js";
3
+ import { type McpToolCatalogSnapshot } from "./mcp-tool-catalog.js";
4
+ export interface McpStdioServer {
5
+ packageRoot: string;
6
+ command: string;
7
+ args: string[];
8
+ cwd: string;
9
+ source: "manifest" | "mcp-json";
10
+ }
11
+ export type McpStdioServerResolution = {
12
+ ok: true;
13
+ server: McpStdioServer;
14
+ } | {
15
+ ok: false;
16
+ message: string;
17
+ };
18
+ export interface McpStdioToolCatalogOptions {
19
+ sandbox?: RuntimeSandboxMode;
20
+ now?: () => number;
21
+ spawnImpl?: typeof spawn;
22
+ removeContainerImpl?: (containerName: string) => Promise<boolean>;
23
+ timeouts?: {
24
+ startupMs?: number;
25
+ requestMs?: number;
26
+ totalMs?: number;
27
+ shutdownMs?: number;
28
+ };
29
+ }
30
+ export declare function resolveMcpStdioServer(targetPath: string, serverName: string): Promise<McpStdioServerResolution>;
31
+ export declare function captureStdioMcpToolCatalog(server: McpStdioServer, options?: McpStdioToolCatalogOptions): Promise<McpToolCatalogSnapshot>;