codex-plugin-doctor 1.60.0 → 1.61.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 +17 -2
- package/dist/core/bounded-http-client.js +1 -0
- package/dist/core/mcp-discovery.d.ts +23 -0
- package/dist/core/mcp-discovery.js +148 -0
- package/dist/reporting/render-mcp-discovery-report.d.ts +2 -0
- package/dist/reporting/render-mcp-discovery-report.js +14 -0
- package/dist/run-cli.d.ts +2 -0
- package/dist/run-cli.js +32 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,21 @@ Runtime MCP validation with `--runtime`:
|
|
|
81
81
|
- optional runtime approval gating with a precomputed `doctor runtime-plan` digest
|
|
82
82
|
- optional Docker isolation for local Node.js stdio servers with `--sandbox docker`
|
|
83
83
|
|
|
84
|
+
### MCP Protocol Discovery
|
|
85
|
+
|
|
86
|
+
Inspect an HTTP MCP endpoint's advertised protocol versions using a bounded MCP
|
|
87
|
+
2026-07-28 `server/discover` request:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
codex-plugin-doctor doctor discover https://mcp.example.com/mcp --allow-network
|
|
91
|
+
codex-plugin-doctor doctor discover https://mcp.example.com/mcp --allow-network --json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This is an opt-in discovery-only check. A successful result validates the discovery
|
|
95
|
+
response, not full protocol conformance, tool behavior, or compatibility with a
|
|
96
|
+
particular client. Existing `check --runtime` remains on its 2025-11-25 baseline.
|
|
97
|
+
See [MCP Discovery](docs/guides/mcp-discovery.md) for consent, scope, and exit codes.
|
|
98
|
+
|
|
84
99
|
### Remote MCP Readiness
|
|
85
100
|
|
|
86
101
|
Remote MCP runtime probing is disabled until you explicitly pass `--runtime --allow-network`. Loopback endpoints also require `--allow-local-network`:
|
|
@@ -499,9 +514,9 @@ jobs:
|
|
|
499
514
|
runs-on: ubuntu-latest
|
|
500
515
|
steps:
|
|
501
516
|
- uses: actions/checkout@v5
|
|
502
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
517
|
+
- uses: Esquetta/CodexPluginDoctor@v1.61.0
|
|
503
518
|
with:
|
|
504
|
-
version: "1.
|
|
519
|
+
version: "1.61.0"
|
|
505
520
|
path: .
|
|
506
521
|
runtime: "true"
|
|
507
522
|
policy: codex-publish
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type RemoteLookup } from "./remote-network-policy.js";
|
|
2
|
+
import type { RemoteMcpRequest } from "./remote-mcp-probe.js";
|
|
3
|
+
import type { Finding } from "../domain/types.js";
|
|
4
|
+
export interface McpDiscoveryOptions {
|
|
5
|
+
allowNetwork?: boolean;
|
|
6
|
+
allowLocalNetwork?: boolean;
|
|
7
|
+
requestTimeoutMs?: number;
|
|
8
|
+
lookup?: RemoteLookup;
|
|
9
|
+
request?: RemoteMcpRequest;
|
|
10
|
+
}
|
|
11
|
+
export interface McpDiscoveryReport {
|
|
12
|
+
schemaVersion: 1;
|
|
13
|
+
requestedVersion: "2026-07-28";
|
|
14
|
+
scope: "discovery-only";
|
|
15
|
+
status: "discovered" | "unsupported" | "blocked" | "failed";
|
|
16
|
+
supportedVersions: string[];
|
|
17
|
+
findings: Finding[];
|
|
18
|
+
coverage: {
|
|
19
|
+
discovery: "pass" | "fail" | "skipped";
|
|
20
|
+
runtime: "not-tested";
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare function discoverMcpServer(rawUrl: string, options?: McpDiscoveryOptions): Promise<McpDiscoveryReport>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { BoundedHttpError, requestBoundedHttp } from "./bounded-http-client.js";
|
|
2
|
+
import { RemoteNetworkPolicyError } from "./remote-network-policy.js";
|
|
3
|
+
import { inspectRemoteMcpUrl } from "./remote-url-policy.js";
|
|
4
|
+
import { packageVersion } from "../version.js";
|
|
5
|
+
const REQUESTED_VERSION = "2026-07-28";
|
|
6
|
+
const REQUEST_ID = "discover-1";
|
|
7
|
+
function finding(id, severity, message, impact, suggestedFix) {
|
|
8
|
+
return { id, severity, message, impact, suggestedFix };
|
|
9
|
+
}
|
|
10
|
+
function report(status, discovery, findings = [], supportedVersions = []) {
|
|
11
|
+
return { schemaVersion: 1, requestedVersion: REQUESTED_VERSION, scope: "discovery-only", status, supportedVersions, findings, coverage: { discovery, runtime: "not-tested" } };
|
|
12
|
+
}
|
|
13
|
+
function isPlainObject(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function parseJsonObject(source) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(source);
|
|
19
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function mediaType(response) {
|
|
26
|
+
const contentType = response.headers["content-type"];
|
|
27
|
+
const value = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
28
|
+
return typeof value === "string" ? value.split(";", 1)[0]?.trim().toLowerCase() ?? null : null;
|
|
29
|
+
}
|
|
30
|
+
function findSseResponse(body) {
|
|
31
|
+
const text = body.toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
32
|
+
let offset = 0;
|
|
33
|
+
while (offset < text.length) {
|
|
34
|
+
const boundary = text.indexOf("\n\n", offset);
|
|
35
|
+
if (boundary === -1)
|
|
36
|
+
return null;
|
|
37
|
+
const data = text.slice(offset, boundary).split("\n")
|
|
38
|
+
.filter((line) => line.startsWith("data:"))
|
|
39
|
+
.map((line) => line.slice(5).replace(/^ /, ""));
|
|
40
|
+
const candidate = data.length === 0 ? null : parseJsonObject(data.join("\n"));
|
|
41
|
+
if (candidate?.id === REQUEST_ID)
|
|
42
|
+
return candidate;
|
|
43
|
+
offset = boundary + 2;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function parseResponse(response) {
|
|
48
|
+
const contentType = mediaType(response);
|
|
49
|
+
return contentType === "application/json" ? parseJsonObject(response.body.toString("utf8"))
|
|
50
|
+
: contentType === "text/event-stream" ? findSseResponse(response.body)
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
function isValidVersion(value) {
|
|
54
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
55
|
+
return false;
|
|
56
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
57
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
58
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
59
|
+
}
|
|
60
|
+
function validVersions(value) {
|
|
61
|
+
return Array.isArray(value) && value.length > 0 && value.length <= 32 && value.every(isValidVersion) && new Set(value).size === value.length;
|
|
62
|
+
}
|
|
63
|
+
function hasMatchingResponseEnvelope(message) {
|
|
64
|
+
return message.jsonrpc === "2.0" && message.id === REQUEST_ID
|
|
65
|
+
&& Object.hasOwn(message, "result") !== Object.hasOwn(message, "error");
|
|
66
|
+
}
|
|
67
|
+
function validDiscoverResult(value) {
|
|
68
|
+
return isPlainObject(value) && value.resultType === "complete" && validVersions(value.supportedVersions)
|
|
69
|
+
&& validCapabilities(value.capabilities) && typeof value.ttlMs === "number" && Number.isFinite(value.ttlMs) && value.ttlMs >= 0
|
|
70
|
+
&& (value.cacheScope === "public" || value.cacheScope === "private") && validOptionalServerInfo(value);
|
|
71
|
+
}
|
|
72
|
+
function validCapabilities(value) {
|
|
73
|
+
return isPlainObject(value) && ["tools", "resources", "prompts", "completions", "logging", "tasks", "extensions"]
|
|
74
|
+
.every((name) => !Object.hasOwn(value, name) || isPlainObject(value[name]));
|
|
75
|
+
}
|
|
76
|
+
function validOptionalServerInfo(value) {
|
|
77
|
+
if (!Object.hasOwn(value, "_meta"))
|
|
78
|
+
return true;
|
|
79
|
+
if (!isPlainObject(value._meta))
|
|
80
|
+
return false;
|
|
81
|
+
const serverInfo = value._meta["io.modelcontextprotocol/serverInfo"];
|
|
82
|
+
return serverInfo === undefined || (isPlainObject(serverInfo) && typeof serverInfo.name === "string" && typeof serverInfo.version === "string");
|
|
83
|
+
}
|
|
84
|
+
function validError(value) {
|
|
85
|
+
return isPlainObject(value) && typeof value.code === "number" && Number.isFinite(value.code) && typeof value.message === "string";
|
|
86
|
+
}
|
|
87
|
+
function unsupportedVersions(error) {
|
|
88
|
+
if (!validError(error) || error.code !== -32022 || !isPlainObject(error.data))
|
|
89
|
+
return null;
|
|
90
|
+
return error.data.requested === REQUESTED_VERSION && validVersions(error.data.supported) ? error.data.supported : null;
|
|
91
|
+
}
|
|
92
|
+
function failureReport(id, message) {
|
|
93
|
+
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.")]);
|
|
94
|
+
}
|
|
95
|
+
function unsupportedReport(id, message, versions = []) {
|
|
96
|
+
return report("unsupported", "skipped", [finding(id, "warn", message, "Discovery-only probing cannot establish support for the requested MCP protocol version.", "Configure a server that supports the requested protocol version, then run discovery again.")], versions);
|
|
97
|
+
}
|
|
98
|
+
export async function discoverMcpServer(rawUrl, options = {}) {
|
|
99
|
+
if (!options.allowNetwork) {
|
|
100
|
+
return report("blocked", "skipped", [finding("plugin.discovery.network_not_approved", "warn", "Remote MCP discovery was not contacted because network access is not approved.", "Discovery would create outbound network traffic without explicit approval.", "Enable network access only after reviewing the endpoint.")]);
|
|
101
|
+
}
|
|
102
|
+
if (inspectRemoteMcpUrl(rawUrl).issues.length > 0) {
|
|
103
|
+
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.")]);
|
|
104
|
+
}
|
|
105
|
+
const request = options.request ?? requestBoundedHttp;
|
|
106
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id: REQUEST_ID, 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
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await request(rawUrl, {
|
|
114
|
+
allowLocalNetwork: options.allowLocalNetwork, lookup: options.lookup, timeoutMs: options.requestTimeoutMs,
|
|
115
|
+
method: "POST", body,
|
|
116
|
+
headers: { Accept: "application/json, text/event-stream", "Content-Type": "application/json", "MCP-Protocol-Version": REQUESTED_VERSION, "Mcp-Method": "server/discover" },
|
|
117
|
+
stopAfter: (received) => findSseResponse(received) !== null
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof RemoteNetworkPolicyError) {
|
|
122
|
+
return report("blocked", "skipped", [finding("plugin.discovery.network.blocked", "warn", "The MCP discovery target is blocked by the remote network policy.", "Discovery could not safely reach the configured target.", "Use an endpoint permitted by the remote network policy.")]);
|
|
123
|
+
}
|
|
124
|
+
return failureReport(error instanceof BoundedHttpError && error.code === "REMOTE_HTTP_TIMEOUT" ? "plugin.discovery.transport.timeout" : "plugin.discovery.transport.failed", "The MCP discovery request did not complete within the configured transport bounds.");
|
|
125
|
+
}
|
|
126
|
+
if (response.statusCode === 401 || response.statusCode === 403) {
|
|
127
|
+
return unsupportedReport("plugin.discovery.authorization.required", "The MCP endpoint requires authorization before discovery can be assessed.");
|
|
128
|
+
}
|
|
129
|
+
const message = parseResponse(response);
|
|
130
|
+
if (!message || !hasMatchingResponseEnvelope(message))
|
|
131
|
+
return failureReport("plugin.discovery.response.invalid", "The MCP endpoint returned an invalid discovery response.");
|
|
132
|
+
if (Object.hasOwn(message, "error")) {
|
|
133
|
+
if (!validError(message.error))
|
|
134
|
+
return failureReport("plugin.discovery.response.error", "The MCP endpoint returned an invalid discovery error.");
|
|
135
|
+
const versions = unsupportedVersions(message.error);
|
|
136
|
+
if (versions !== null && response.statusCode === 400)
|
|
137
|
+
return unsupportedReport("plugin.discovery.protocol_version.unsupported", "The MCP endpoint does not support the requested protocol version.", versions);
|
|
138
|
+
if (message.error.code === -32601 && (response.statusCode === 200 || response.statusCode === 404))
|
|
139
|
+
return unsupportedReport("plugin.discovery.method.unsupported", "The MCP endpoint does not implement server discovery for this protocol version.");
|
|
140
|
+
return failureReport("plugin.discovery.response.error", "The MCP endpoint returned an unrecognized discovery error.");
|
|
141
|
+
}
|
|
142
|
+
if (response.statusCode !== 200 || !validDiscoverResult(message.result))
|
|
143
|
+
return failureReport("plugin.discovery.result.invalid", "The MCP endpoint returned an invalid discovery result.");
|
|
144
|
+
const versions = message.result.supportedVersions;
|
|
145
|
+
if (!versions.includes(REQUESTED_VERSION))
|
|
146
|
+
return unsupportedReport("plugin.discovery.protocol_version.inconsistent", "The MCP endpoint completed discovery without listing the requested protocol version.", versions);
|
|
147
|
+
return report("discovered", "pass", [], versions);
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function renderMcpDiscoveryReport(report) {
|
|
2
|
+
return [
|
|
3
|
+
"MCP Discovery",
|
|
4
|
+
"=============",
|
|
5
|
+
`Status: ${report.status}`,
|
|
6
|
+
`Scope: ${report.scope}`,
|
|
7
|
+
`Requested protocol: ${report.requestedVersion}`,
|
|
8
|
+
`Advertised versions: ${report.supportedVersions.join(", ") || "not available"}`,
|
|
9
|
+
`Discovery response: ${report.coverage.discovery}`,
|
|
10
|
+
`Runtime: ${report.coverage.runtime}`,
|
|
11
|
+
"Discovery does not establish full protocol conformance or client compatibility.",
|
|
12
|
+
...report.findings.map((finding) => `${finding.severity.toUpperCase()} ${finding.id}: ${finding.message}\n ${finding.suggestedFix}`)
|
|
13
|
+
].join("\n");
|
|
14
|
+
}
|
package/dist/run-cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { discoverMcpServer } from "./core/mcp-discovery.js";
|
|
1
2
|
import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport } from "./core/corpus-quality-metrics.js";
|
|
2
3
|
import { runCheck } from "./index.js";
|
|
3
4
|
import { writeRawDoctorConfig } from "./index.js";
|
|
@@ -13,6 +14,7 @@ export interface CliTerminalContext {
|
|
|
13
14
|
platform?: NodeJS.Platform;
|
|
14
15
|
}
|
|
15
16
|
export interface RunCliOptions {
|
|
17
|
+
discoverMcpServerImpl?: typeof discoverMcpServer;
|
|
16
18
|
terminalContext?: CliTerminalContext;
|
|
17
19
|
runCheckImpl?: typeof runCheck;
|
|
18
20
|
writeRawDoctorConfigImpl?: typeof writeRawDoctorConfig;
|
package/dist/run-cli.js
CHANGED
|
@@ -21,6 +21,8 @@ import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
|
|
|
21
21
|
import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
|
|
22
22
|
import { buildSubmissionArchivePreflight, submissionArchiveExitCode } from "./core/submission-archive-preflight.js";
|
|
23
23
|
import { buildSubmissionPreflight } from "./core/submission-preflight.js";
|
|
24
|
+
import { discoverMcpServer } from "./core/mcp-discovery.js";
|
|
25
|
+
import { renderMcpDiscoveryReport } from "./reporting/render-mcp-discovery-report.js";
|
|
24
26
|
import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
|
|
25
27
|
import { buildExternalValidationCorpusReport, ExternalCorpusManifestError } from "./core/external-validation-corpus.js";
|
|
26
28
|
import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport, CorpusMetricsDiffError, CorpusMetricsManifestError, renderCorpusQualityMetricsDiffJson, renderCorpusQualityMetricsDiffMarkdown, renderCorpusQualityMetricsDiffText, renderCorpusQualityMetricsJson, renderCorpusQualityMetricsMarkdown, renderCorpusQualityMetricsText } from "./core/corpus-quality-metrics.js";
|
|
@@ -99,6 +101,7 @@ function writeExactStdout(io, message) {
|
|
|
99
101
|
}
|
|
100
102
|
class CliUsageError extends Error {
|
|
101
103
|
}
|
|
104
|
+
const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json]";
|
|
102
105
|
function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
|
|
103
106
|
const index = flags.indexOf("--sandbox");
|
|
104
107
|
if (index === -1) {
|
|
@@ -181,6 +184,7 @@ function parseRemoteNetworkFlags(flags, runtime) {
|
|
|
181
184
|
}
|
|
182
185
|
function printUsage(io) {
|
|
183
186
|
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>] [--fail-on <rule-id>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
187
|
+
io.writeStderr(discoveryUsage);
|
|
184
188
|
io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
|
|
185
189
|
+ " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
|
|
186
190
|
+ "Registry publication preflight: codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
|
|
@@ -1191,6 +1195,34 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1191
1195
|
return 0;
|
|
1192
1196
|
}
|
|
1193
1197
|
if (command === "doctor") {
|
|
1198
|
+
if (maybePath === "discover") {
|
|
1199
|
+
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1200
|
+
io.writeStdout(`${discoveryUsage}\nHTTP discovery only; runtime behavior is not tested.`);
|
|
1201
|
+
return 0;
|
|
1202
|
+
}
|
|
1203
|
+
const [url, ...flags] = remainingArgs;
|
|
1204
|
+
if (!url || url.startsWith("-")) {
|
|
1205
|
+
io.writeStderr(discoveryUsage);
|
|
1206
|
+
return 2;
|
|
1207
|
+
}
|
|
1208
|
+
const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
|
|
1209
|
+
if (flags.some((flag) => !allowedFlags.has(flag)) || new Set(flags).size !== flags.length) {
|
|
1210
|
+
io.writeStderr(`Invalid or duplicate discovery arguments. ${discoveryUsage}`);
|
|
1211
|
+
return 2;
|
|
1212
|
+
}
|
|
1213
|
+
if (!flags.includes("--allow-network")) {
|
|
1214
|
+
io.writeStderr("doctor discover requires explicit --allow-network consent.");
|
|
1215
|
+
return 2;
|
|
1216
|
+
}
|
|
1217
|
+
const report = await (options.discoverMcpServerImpl ?? discoverMcpServer)(url, {
|
|
1218
|
+
allowNetwork: true,
|
|
1219
|
+
allowLocalNetwork: flags.includes("--allow-local-network")
|
|
1220
|
+
});
|
|
1221
|
+
io.writeStdout(flags.includes("--json")
|
|
1222
|
+
? JSON.stringify(report, null, 2)
|
|
1223
|
+
: renderMcpDiscoveryReport(report));
|
|
1224
|
+
return report.status === "discovered" ? 0 : report.status === "unsupported" ? 1 : 2;
|
|
1225
|
+
}
|
|
1194
1226
|
const doctorFlags = maybePath?.startsWith("--")
|
|
1195
1227
|
? [maybePath, ...remainingArgs]
|
|
1196
1228
|
: remainingArgs;
|
package/package.json
CHANGED