codex-plugin-doctor 1.54.0 → 1.55.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
@@ -108,6 +108,32 @@ codex-plugin-doctor registry inspect io.github.example/weather --allow-network
108
108
 
109
109
  Local checks validate metadata, ownership consistency, package integrity, transports, and Codex installability. Exact Registry lookup is read-only, requires explicit network consent, and contacts only the fixed official Registry endpoint. It never follows advertised package, icon, repository, or remote MCP URLs. See [MCP Registry Readiness](./docs/architecture/mcp-registry-readiness.md).
110
110
 
111
+ ### MCP Registry Publication Preflight
112
+
113
+ Check a publication candidate locally without making a network request:
114
+
115
+ ```bash
116
+ codex-plugin-doctor registry preflight path/to/server.json --json
117
+ ```
118
+
119
+ Opt in to bounded public npm and MCP Registry metadata checks:
120
+
121
+ ```bash
122
+ codex-plugin-doctor registry preflight path/to/server.json --allow-network --json
123
+ ```
124
+
125
+ Require a publish-ready result in CI:
126
+
127
+ ```bash
128
+ codex-plugin-doctor registry preflight path/to/server.json --allow-network --require-publish-ready --json
129
+ ```
130
+
131
+ Preflight checks the exact npm package version, then the exact Registry version
132
+ and latest Registry record on fixed public hosts. An existing exact Registry
133
+ version is an immutable collision and must be replaced by a version bump. The
134
+ command is advisory: it never authenticates, publishes, or changes npm or
135
+ Registry records. See [MCP Registry Publication Preflight](./docs/architecture/mcp-registry-publication-preflight.md).
136
+
111
137
  Output formats:
112
138
 
113
139
  - human text output
@@ -454,9 +480,9 @@ jobs:
454
480
  runs-on: ubuntu-latest
455
481
  steps:
456
482
  - uses: actions/checkout@v5
457
- - uses: Esquetta/CodexPluginDoctor@v1.54.0
483
+ - uses: Esquetta/CodexPluginDoctor@v1.55.0
458
484
  with:
459
- version: "1.54.0"
485
+ version: "1.55.0"
460
486
  path: .
461
487
  runtime: "true"
462
488
  policy: codex-publish
@@ -0,0 +1,38 @@
1
+ import { type BoundedHttpRequestOptions, type BoundedHttpResponse } from "./bounded-http-client.js";
2
+ export type McpRegistryPublicationPreflightStatus = "pass" | "warn" | "fail";
3
+ type RegistryRequest = (url: string, options?: BoundedHttpRequestOptions) => Promise<BoundedHttpResponse>;
4
+ export interface McpRegistryPublicationPreflightFinding {
5
+ id: string;
6
+ severity: "warn" | "fail";
7
+ message: string;
8
+ }
9
+ export interface McpRegistryPublicationPreflightReport {
10
+ schemaVersion: "1.0.0";
11
+ kind: "mcp-registry-publication-preflight";
12
+ generatedAt: string;
13
+ target: "server.json";
14
+ serverName?: string;
15
+ serverVersion?: string;
16
+ status: McpRegistryPublicationPreflightStatus;
17
+ localReadiness: McpRegistryPublicationPreflightStatus;
18
+ packagePublication: "pass" | "fail" | "skipped" | "unknown";
19
+ registryVersionAvailability: "available-first-publication" | "available-new-version" | "already-published" | "unknown";
20
+ publisherPlan: {
21
+ executable: false;
22
+ steps: Array<{
23
+ order: number;
24
+ command: "mcp-publisher login github" | "mcp-publisher publish";
25
+ purpose: string;
26
+ }>;
27
+ };
28
+ findings: McpRegistryPublicationPreflightFinding[];
29
+ }
30
+ export interface BuildMcpRegistryPublicationPreflightOptions {
31
+ allowNetwork?: boolean;
32
+ request?: RegistryRequest;
33
+ }
34
+ export declare function buildMcpRegistryPublicationPreflight(targetPath: string, options?: BuildMcpRegistryPublicationPreflightOptions): Promise<McpRegistryPublicationPreflightReport>;
35
+ export declare function renderMcpRegistryPublicationPreflightJson(report: McpRegistryPublicationPreflightReport): string;
36
+ export declare function renderMcpRegistryPublicationPreflight(report: McpRegistryPublicationPreflightReport): string;
37
+ export declare function registryPublicationPreflightExitCode(report: McpRegistryPublicationPreflightReport, requirePublishReady?: boolean): 0 | 1;
38
+ export {};
@@ -0,0 +1,358 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { buildMcpRegistryReadiness } from "./mcp-registry.js";
4
+ import { requestBoundedHttp } from "./bounded-http-client.js";
5
+ const SERVER_NAME_PATTERN = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
6
+ const VERSION_RANGE_PATTERN = /^(?:latest|[~^]|[<>]=?)|(?:\s+\|\|\s+)|(?:^|[.\s])[x*](?:$|[.\s])/i;
7
+ const OFFICIAL_SCHEMA_PATTERN = /^https:\/\/static\.modelcontextprotocol\.io\/schemas\/\d{4}-\d{2}-\d{2}\/server\.schema\.json$/;
8
+ const NPM_IDENTIFIER_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]{0,213}\/)?[a-z0-9][a-z0-9._-]{0,213}$/;
9
+ const INTEGRITY_PATTERN = /^(sha256|sha384|sha512)-([A-Za-z0-9+/]*={0,2})$/;
10
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
11
+ const NPM_REGISTRY = "https://registry.npmjs.org";
12
+ const MCP_REGISTRY = "https://registry.modelcontextprotocol.io";
13
+ const REQUEST_OPTIONS = {
14
+ method: "GET",
15
+ headers: {
16
+ accept: "application/json",
17
+ "user-agent": "codex-plugin-doctor"
18
+ }
19
+ };
20
+ function isRecord(value) {
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ }
23
+ function publicFinding(finding) {
24
+ return {
25
+ id: finding.id,
26
+ severity: finding.severity,
27
+ message: finding.message
28
+ };
29
+ }
30
+ function statusFromFindings(findings) {
31
+ if (findings.some((finding) => finding.severity === "fail")) {
32
+ return "fail";
33
+ }
34
+ return findings.some((finding) => finding.severity === "warn") ? "warn" : "pass";
35
+ }
36
+ function isSafeServerName(value) {
37
+ return value !== undefined && value.length >= 3 && value.length <= 200 && SERVER_NAME_PATTERN.test(value);
38
+ }
39
+ function isSafeServerVersion(value) {
40
+ return value !== undefined && value.length >= 1 && value.length <= 255
41
+ && !/[\\/\u0000-\u001F\u007F]/.test(value) && !VERSION_RANGE_PATTERN.test(value);
42
+ }
43
+ function isSafeNpmIdentifier(value) {
44
+ return typeof value === "string" && NPM_IDENTIFIER_PATTERN.test(value);
45
+ }
46
+ function isIntegrity(value) {
47
+ if (typeof value !== "string") {
48
+ return false;
49
+ }
50
+ const match = INTEGRITY_PATTERN.exec(value);
51
+ if (!match || match[2].length % 4 !== 0) {
52
+ return false;
53
+ }
54
+ const digest = Buffer.from(match[2], "base64");
55
+ const expectedBytes = match[1] === "sha256" ? 32 : match[1] === "sha384" ? 48 : 64;
56
+ return digest.length === expectedBytes && digest.toString("base64") === match[2];
57
+ }
58
+ function responseJson(response) {
59
+ try {
60
+ return JSON.parse(response.body.toString("utf8"));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function validNotFound(response) {
67
+ const payload = responseJson(response);
68
+ if (response.statusCode !== 404 || !isRecord(payload)) {
69
+ return false;
70
+ }
71
+ return (typeof payload.error === "string" && payload.error.trim().length > 0)
72
+ || (payload.title === "Not Found"
73
+ && payload.status === 404
74
+ && typeof payload.detail === "string"
75
+ && payload.detail.trim().length > 0);
76
+ }
77
+ function hasValidHttpUrl(value) {
78
+ if (typeof value !== "string" || value.length === 0 || /\s/.test(value)) {
79
+ return false;
80
+ }
81
+ try {
82
+ const url = new URL(value.replace(/\{[^{}]+\}/g, "template-value"));
83
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function hasValidPackageShape(value) {
90
+ if (!isRecord(value)
91
+ || typeof value.registryType !== "string" || value.registryType.length === 0
92
+ || typeof value.identifier !== "string" || value.identifier.length === 0
93
+ || !isRecord(value.transport)
94
+ || !["stdio", "streamable-http", "sse"].includes(value.transport.type)) {
95
+ return false;
96
+ }
97
+ if (/^https?:\/\//i.test(value.identifier) && !hasValidHttpUrl(value.identifier)) {
98
+ return false;
99
+ }
100
+ if (typeof value.version === "string" && VERSION_RANGE_PATTERN.test(value.version)) {
101
+ return false;
102
+ }
103
+ return value.registryType !== "mcpb" || (typeof value.fileSha256 === "string" && SHA256_PATTERN.test(value.fileSha256));
104
+ }
105
+ function hasValidRemoteShape(value) {
106
+ return isRecord(value)
107
+ && (value.type === "streamable-http" || value.type === "sse")
108
+ && hasValidHttpUrl(value.url);
109
+ }
110
+ function hasValidRegistryServerShape(server) {
111
+ return typeof server.$schema === "string"
112
+ && OFFICIAL_SCHEMA_PATTERN.test(server.$schema)
113
+ && isSafeServerName(typeof server.name === "string" ? server.name : undefined)
114
+ && isSafeServerVersion(typeof server.version === "string" ? server.version : undefined)
115
+ && typeof server.description === "string"
116
+ && server.description.length >= 1
117
+ && server.description.length <= 100
118
+ && (server.packages === undefined || (Array.isArray(server.packages) && server.packages.every(hasValidPackageShape)))
119
+ && (server.remotes === undefined || (Array.isArray(server.remotes) && server.remotes.every(hasValidRemoteShape)));
120
+ }
121
+ function matchingRegistryServer(response, serverName, serverVersion) {
122
+ const payload = responseJson(response);
123
+ if (response.statusCode !== 200 || !isRecord(payload) || !isRecord(payload.server)) {
124
+ return false;
125
+ }
126
+ const server = payload.server;
127
+ return hasValidRegistryServerShape(server)
128
+ && server.name === serverName
129
+ && (serverVersion === undefined || server.version === serverVersion);
130
+ }
131
+ function matchingNpmPackument(response, packageName, packageVersion, serverName) {
132
+ const payload = responseJson(response);
133
+ if (response.statusCode !== 200 || !isRecord(payload) || payload.name !== packageName || !isRecord(payload.versions)) {
134
+ return false;
135
+ }
136
+ const version = payload.versions[packageVersion];
137
+ return isRecord(version)
138
+ && version.name === packageName
139
+ && version.version === packageVersion
140
+ && version.mcpName === serverName
141
+ && isRecord(version.dist)
142
+ && isIntegrity(version.dist.integrity);
143
+ }
144
+ function addFinding(findings, id, message) {
145
+ findings.push({ id, severity: "fail", message });
146
+ }
147
+ async function resolveServerJsonPath(targetPath) {
148
+ const resolved = path.resolve(targetPath);
149
+ try {
150
+ return (await stat(resolved)).isDirectory() ? path.join(resolved, "server.json") : resolved;
151
+ }
152
+ catch {
153
+ return resolved.endsWith(".json") ? resolved : path.join(resolved, "server.json");
154
+ }
155
+ }
156
+ async function readJson(pathname) {
157
+ try {
158
+ const parsed = JSON.parse(await readFile(pathname, "utf8"));
159
+ return isRecord(parsed) ? parsed : null;
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ function npmDeclarations(server) {
166
+ return Array.isArray(server?.packages)
167
+ ? server.packages.filter((entry) => isRecord(entry) && entry.registryType === "npm")
168
+ : [];
169
+ }
170
+ function publisherPlan() {
171
+ return {
172
+ executable: false,
173
+ steps: [
174
+ {
175
+ order: 1,
176
+ command: "mcp-publisher login github",
177
+ purpose: "Authenticate with GitHub for Registry publication."
178
+ },
179
+ {
180
+ order: 2,
181
+ command: "mcp-publisher publish",
182
+ purpose: "Publish the validated Registry metadata."
183
+ }
184
+ ]
185
+ };
186
+ }
187
+ export async function buildMcpRegistryPublicationPreflight(targetPath, options = {}) {
188
+ void options;
189
+ const readiness = await buildMcpRegistryReadiness(targetPath);
190
+ const findings = readiness.findings.map(publicFinding);
191
+ const serverJsonPath = await resolveServerJsonPath(targetPath);
192
+ const server = await readJson(serverJsonPath);
193
+ const declarations = npmDeclarations(server);
194
+ const packageJson = await readJson(path.join(path.dirname(serverJsonPath), "package.json"));
195
+ if (declarations.length > 1) {
196
+ findings.push({
197
+ id: "registry.preflight.package.multiple-npm-declarations",
198
+ severity: "fail",
199
+ message: "Publication evidence supports exactly one npm package declaration."
200
+ });
201
+ }
202
+ else if (declarations.length === 1
203
+ && typeof declarations[0].identifier === "string"
204
+ && packageJson
205
+ && packageJson.name !== declarations[0].identifier) {
206
+ findings.push({
207
+ id: "registry.preflight.package.local-name-mismatch",
208
+ severity: "fail",
209
+ message: "package.json name must match the declared npm package identifier."
210
+ });
211
+ }
212
+ if (declarations.length === 1 && !isSafeNpmIdentifier(declarations[0].identifier)) {
213
+ findings.push({
214
+ id: "registry.preflight.package.invalid-npm-identifier",
215
+ severity: "fail",
216
+ message: "The declared npm package identifier is not valid for public registry verification."
217
+ });
218
+ }
219
+ const localReadiness = statusFromFindings(findings);
220
+ let packagePublication = declarations.length === 0
221
+ ? "skipped"
222
+ : localReadiness === "fail" ? "fail" : "unknown";
223
+ let registryVersionAvailability = "unknown";
224
+ const declaration = declarations.length === 1 ? declarations[0] : undefined;
225
+ const serverName = isSafeServerName(readiness.serverName) ? readiness.serverName : undefined;
226
+ const serverVersion = isSafeServerVersion(readiness.serverVersion) ? readiness.serverVersion : undefined;
227
+ const packageName = isSafeNpmIdentifier(declaration?.identifier) ? declaration.identifier : undefined;
228
+ const declarationVersion = typeof declaration?.version === "string" ? declaration.version : undefined;
229
+ const packageVersion = isSafeServerVersion(declarationVersion)
230
+ ? declarationVersion
231
+ : undefined;
232
+ const canVerifyNetwork = options.allowNetwork === true
233
+ && localReadiness === "pass"
234
+ && declaration !== undefined
235
+ && serverName !== undefined
236
+ && serverVersion !== undefined
237
+ && packageName !== undefined
238
+ && packageVersion !== undefined;
239
+ if (!canVerifyNetwork && localReadiness !== "fail") {
240
+ findings.push({
241
+ id: "registry.preflight.network-unverified",
242
+ severity: "warn",
243
+ message: "Package publication and Registry version availability require explicit network verification."
244
+ });
245
+ }
246
+ if (canVerifyNetwork && packageName && packageVersion && serverName && serverVersion) {
247
+ const request = options.request ?? requestBoundedHttp;
248
+ let npmResponse;
249
+ try {
250
+ npmResponse = await request(`${NPM_REGISTRY}/${encodeURIComponent(packageName)}`, REQUEST_OPTIONS);
251
+ }
252
+ catch {
253
+ addFinding(findings, "registry.preflight.npm.request", "Public npm metadata could not be verified.");
254
+ packagePublication = "unknown";
255
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
256
+ }
257
+ if (npmResponse.statusCode !== 200 && npmResponse.statusCode !== 404) {
258
+ addFinding(findings, "registry.preflight.npm.response", "Public npm metadata could not prove package publication.");
259
+ packagePublication = "unknown";
260
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
261
+ }
262
+ if (!matchingNpmPackument(npmResponse, packageName, packageVersion, serverName)) {
263
+ addFinding(findings, "registry.preflight.npm.metadata", "Published npm metadata does not match the declared package version.");
264
+ packagePublication = "fail";
265
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
266
+ }
267
+ packagePublication = "pass";
268
+ let exactResponse;
269
+ try {
270
+ exactResponse = await request(`${MCP_REGISTRY}/v0.1/servers/${encodeURIComponent(serverName)}/versions/${encodeURIComponent(serverVersion)}`, REQUEST_OPTIONS);
271
+ }
272
+ catch {
273
+ addFinding(findings, "registry.preflight.registry.exact-request", "Exact Registry version availability could not be verified.");
274
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
275
+ }
276
+ if (matchingRegistryServer(exactResponse, serverName, serverVersion)) {
277
+ registryVersionAvailability = "already-published";
278
+ addFinding(findings, "registry.preflight.registry.already-published", "The exact Registry version is already published and cannot be overwritten.");
279
+ }
280
+ else if (validNotFound(exactResponse)) {
281
+ let latestResponse;
282
+ try {
283
+ latestResponse = await request(`${MCP_REGISTRY}/v0.1/servers/${encodeURIComponent(serverName)}/versions/latest`, REQUEST_OPTIONS);
284
+ }
285
+ catch {
286
+ addFinding(findings, "registry.preflight.registry.latest-request", "Latest Registry version availability could not be verified.");
287
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
288
+ }
289
+ if (validNotFound(latestResponse)) {
290
+ registryVersionAvailability = "available-first-publication";
291
+ }
292
+ else if (matchingRegistryServer(latestResponse, serverName) && responseJson(latestResponse) !== null) {
293
+ const payload = responseJson(latestResponse);
294
+ const latestVersion = payload.server.version;
295
+ if (latestVersion !== serverVersion) {
296
+ registryVersionAvailability = "available-new-version";
297
+ }
298
+ else {
299
+ addFinding(findings, "registry.preflight.registry.latest-response", "Latest Registry metadata did not prove a different published version.");
300
+ }
301
+ }
302
+ else {
303
+ addFinding(findings, "registry.preflight.registry.latest-response", "Latest Registry metadata could not prove version availability.");
304
+ }
305
+ }
306
+ else {
307
+ addFinding(findings, "registry.preflight.registry.exact-response", "Exact Registry metadata could not prove version availability.");
308
+ }
309
+ }
310
+ return buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings);
311
+ }
312
+ function buildReport(readiness, localReadiness, packagePublication, registryVersionAvailability, findings) {
313
+ return {
314
+ schemaVersion: "1.0.0",
315
+ kind: "mcp-registry-publication-preflight",
316
+ generatedAt: new Date().toISOString(),
317
+ target: "server.json",
318
+ ...(isSafeServerName(readiness.serverName) ? { serverName: readiness.serverName } : {}),
319
+ ...(isSafeServerVersion(readiness.serverVersion) ? { serverVersion: readiness.serverVersion } : {}),
320
+ status: statusFromFindings(findings),
321
+ localReadiness,
322
+ packagePublication,
323
+ registryVersionAvailability,
324
+ publisherPlan: publisherPlan(),
325
+ findings
326
+ };
327
+ }
328
+ export function renderMcpRegistryPublicationPreflightJson(report) {
329
+ return JSON.stringify(report, null, 2);
330
+ }
331
+ export function renderMcpRegistryPublicationPreflight(report) {
332
+ const networkVerification = report.findings.some((finding) => finding.id === "registry.preflight.network-unverified")
333
+ ? "NOT REQUESTED"
334
+ : report.packagePublication === "pass" && report.registryVersionAvailability !== "unknown"
335
+ ? "COMPLETED"
336
+ : report.localReadiness === "fail"
337
+ ? "NOT AVAILABLE"
338
+ : "INCOMPLETE";
339
+ const lines = [
340
+ `Registry publication preflight: ${report.status.toUpperCase()}`,
341
+ `Target: ${report.target}`,
342
+ report.serverName ? `Server: ${report.serverName}@${report.serverVersion ?? "unknown"}` : null,
343
+ `Local readiness: ${report.localReadiness.toUpperCase()}`,
344
+ `Package publication: ${report.packagePublication.toUpperCase()}`,
345
+ `Registry version availability: ${report.registryVersionAvailability.toUpperCase()}`,
346
+ `Network verification: ${networkVerification}`
347
+ ].filter((line) => line !== null);
348
+ if (report.findings.length > 0) {
349
+ lines.push("", "Findings", "--------");
350
+ for (const finding of report.findings) {
351
+ lines.push(`${finding.severity.toUpperCase()} ${finding.id}: ${finding.message}`);
352
+ }
353
+ }
354
+ return lines.join("\n");
355
+ }
356
+ export function registryPublicationPreflightExitCode(report, requirePublishReady = false) {
357
+ return report.status === "fail" || (requirePublishReady && report.status !== "pass") ? 1 : 0;
358
+ }
@@ -9,6 +9,68 @@ const remoteTransportReliabilityStatusSchema = {
9
9
  type: "string",
10
10
  enum: ["pass", "warn", "fail", "skipped"]
11
11
  };
12
+ const registryPublicationPreflightStatusSchema = {
13
+ type: "string",
14
+ enum: ["pass", "warn", "fail"]
15
+ };
16
+ const registryPackagePublicationSchema = {
17
+ type: "string",
18
+ enum: ["pass", "fail", "skipped", "unknown"]
19
+ };
20
+ const registryVersionAvailabilitySchema = {
21
+ type: "string",
22
+ enum: ["available-first-publication", "available-new-version", "already-published", "unknown"]
23
+ };
24
+ const registryPublisherPlanSchema = {
25
+ type: "object",
26
+ properties: {
27
+ executable: {
28
+ const: false
29
+ },
30
+ steps: {
31
+ type: "array",
32
+ minItems: 2,
33
+ maxItems: 2,
34
+ prefixItems: [
35
+ {
36
+ type: "object",
37
+ properties: {
38
+ order: {
39
+ const: 1
40
+ },
41
+ command: {
42
+ const: "mcp-publisher login github"
43
+ },
44
+ purpose: {
45
+ type: "string"
46
+ }
47
+ },
48
+ required: ["order", "command", "purpose"],
49
+ additionalProperties: false
50
+ },
51
+ {
52
+ type: "object",
53
+ properties: {
54
+ order: {
55
+ const: 2
56
+ },
57
+ command: {
58
+ const: "mcp-publisher publish"
59
+ },
60
+ purpose: {
61
+ type: "string"
62
+ }
63
+ },
64
+ required: ["order", "command", "purpose"],
65
+ additionalProperties: false
66
+ }
67
+ ],
68
+ items: false
69
+ }
70
+ },
71
+ required: ["executable", "steps"],
72
+ additionalProperties: false
73
+ };
12
74
  const runtimeConformanceSchema = {
13
75
  type: "object",
14
76
  properties: {
@@ -198,6 +260,36 @@ const publicSchemaDefinitions = [
198
260
  }
199
261
  }
200
262
  },
263
+ {
264
+ id: "doctor.registry.preflight.json",
265
+ command: "codex-plugin-doctor registry preflight <server.json|directory> --json",
266
+ outputKind: "mcp-registry-publication-preflight",
267
+ required: [
268
+ "schemaVersion",
269
+ "kind",
270
+ "generatedAt",
271
+ "target",
272
+ "status",
273
+ "localReadiness",
274
+ "packagePublication",
275
+ "registryVersionAvailability",
276
+ "publisherPlan",
277
+ "findings"
278
+ ],
279
+ properties: {
280
+ target: {
281
+ const: "server.json"
282
+ },
283
+ status: registryPublicationPreflightStatusSchema,
284
+ localReadiness: registryPublicationPreflightStatusSchema,
285
+ packagePublication: registryPackagePublicationSchema,
286
+ registryVersionAvailability: registryVersionAvailabilitySchema,
287
+ publisherPlan: registryPublisherPlanSchema,
288
+ findings: {
289
+ type: "array"
290
+ }
291
+ }
292
+ },
201
293
  {
202
294
  id: "doctor.audit.json",
203
295
  command: "codex-plugin-doctor audit --installed --json",
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, ty
31
31
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
32
32
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
33
33
  export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson, type InspectMcpRegistryOptions, type McpRegistryFinding, type McpRegistryInstallability, type McpRegistryReadinessReport, type McpRegistryScorecard } from "./core/mcp-registry.js";
34
+ export { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson, type BuildMcpRegistryPublicationPreflightOptions, type McpRegistryPublicationPreflightFinding, type McpRegistryPublicationPreflightReport, type McpRegistryPublicationPreflightStatus } from "./core/mcp-registry-preflight.js";
34
35
  export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
35
36
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditRecommendation, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
36
37
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } f
31
31
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
32
32
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
33
33
  export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
34
+ export { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson } from "./core/mcp-registry-preflight.js";
34
35
  export { watchPlugin } from "./core/watch-plugin.js";
35
36
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
36
37
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
package/dist/run-cli.js CHANGED
@@ -29,6 +29,7 @@ import { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBun
29
29
  import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
30
30
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
31
31
  import { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
32
+ import { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson } from "./core/mcp-registry-preflight.js";
32
33
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
33
34
  import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
34
35
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
@@ -169,7 +170,8 @@ function parseRemoteNetworkFlags(flags, runtime) {
169
170
  function printUsage(io) {
170
171
  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>]\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");
171
172
  io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
172
- + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]");
173
+ + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
174
+ + "Registry publication preflight: codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
173
175
  io.writeStderr("Corpus quality regression: codex-plugin-doctor doctor corpus metrics diff --before <metrics.json> --after <metrics.json> [--fail-on-regression] [--json|--markdown] [--output <path>]");
174
176
  io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
175
177
  io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
@@ -2363,16 +2365,18 @@ export async function runCli(args, io = defaultIo, options = {}) {
2363
2365
  const subcommand = maybePath;
2364
2366
  const target = remainingArgs[0];
2365
2367
  const flags = remainingArgs.slice(1);
2366
- if ((subcommand !== "check" && subcommand !== "inspect") || !target || target.startsWith("--")) {
2368
+ if ((subcommand !== "check" && subcommand !== "inspect" && subcommand !== "preflight") || !target || target.startsWith("--")) {
2367
2369
  io.writeStderr("Usage: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
2368
- + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]");
2370
+ + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
2371
+ + " codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
2369
2372
  return 2;
2370
2373
  }
2371
2374
  const allowedFlags = new Set([
2372
2375
  "--json",
2373
2376
  "--output",
2374
2377
  "--allow-network",
2375
- "--require-registry-readiness"
2378
+ "--require-registry-readiness",
2379
+ "--require-publish-ready"
2376
2380
  ]);
2377
2381
  let outputPath = null;
2378
2382
  for (let index = 0; index < flags.length; index += 1) {
@@ -2393,14 +2397,33 @@ export async function runCli(args, io = defaultIo, options = {}) {
2393
2397
  }
2394
2398
  const allowNetwork = flags.includes("--allow-network");
2395
2399
  if (subcommand === "check" && allowNetwork) {
2396
- io.writeStderr("--allow-network is supported only by registry inspect.");
2400
+ io.writeStderr("--allow-network is not supported by registry check.");
2397
2401
  return 2;
2398
2402
  }
2399
2403
  if (subcommand === "inspect" && !allowNetwork) {
2400
2404
  io.writeStderr("registry inspect requires explicit --allow-network consent.");
2401
2405
  return 2;
2402
2406
  }
2407
+ if (subcommand === "preflight" && flags.includes("--require-registry-readiness")) {
2408
+ io.writeStderr("--require-registry-readiness is supported only by registry check or registry inspect.");
2409
+ return 2;
2410
+ }
2411
+ if (subcommand !== "preflight" && flags.includes("--require-publish-ready")) {
2412
+ io.writeStderr("--require-publish-ready is supported only by registry preflight.");
2413
+ return 2;
2414
+ }
2403
2415
  try {
2416
+ if (subcommand === "preflight") {
2417
+ const report = await buildMcpRegistryPublicationPreflight(target, { allowNetwork });
2418
+ const rendered = flags.includes("--json")
2419
+ ? renderMcpRegistryPublicationPreflightJson(report)
2420
+ : renderMcpRegistryPublicationPreflight(report);
2421
+ if (outputPath) {
2422
+ await writeFile(outputPath, rendered, "utf8");
2423
+ }
2424
+ io.writeStdout(rendered);
2425
+ return registryPublicationPreflightExitCode(report, flags.includes("--require-publish-ready"));
2426
+ }
2404
2427
  const report = subcommand === "check"
2405
2428
  ? await buildMcpRegistryReadiness(target)
2406
2429
  : await inspectMcpRegistryServer(target, { allowNetwork: true });
@@ -2414,7 +2437,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
2414
2437
  return registryReadinessExitCode(report, flags.includes("--require-registry-readiness"));
2415
2438
  }
2416
2439
  catch (error) {
2417
- io.writeStderr(`Registry inspection failed: ${error.message}`);
2440
+ if (subcommand === "preflight") {
2441
+ io.writeStderr("Registry publication preflight failed.");
2442
+ }
2443
+ else {
2444
+ io.writeStderr(`Registry inspection failed: ${error.message}`);
2445
+ }
2418
2446
  return 1;
2419
2447
  }
2420
2448
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.54.0",
3
+ "version": "1.55.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",