codex-plugin-doctor 1.53.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.
@@ -0,0 +1,455 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { requestBoundedHttp } from "./bounded-http-client.js";
4
+ const OFFICIAL_SCHEMA = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";
5
+ const OFFICIAL_SCHEMA_PATTERN = /^https:\/\/static\.modelcontextprotocol\.io\/schemas\/\d{4}-\d{2}-\d{2}\/server\.schema\.json$/;
6
+ const OFFICIAL_REGISTRY = "https://registry.modelcontextprotocol.io";
7
+ const SERVER_NAME_PATTERN = /^[a-zA-Z0-9.-]+\/[a-zA-Z0-9._-]+$/;
8
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
9
+ const VERSION_RANGE_PATTERN = /^(?:latest|[~^]|[<>]=?)|(?:\s+\|\|\s+)|(?:^|[.\s])[x*](?:$|[.\s])/i;
10
+ const SECRET_NAME_PATTERN = /(?:api[_-]?key|token|secret|password|credential)/i;
11
+ function isRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function statusFromFindings(findings) {
15
+ if (findings.some((finding) => finding.severity === "fail")) {
16
+ return "fail";
17
+ }
18
+ return findings.length > 0 ? "warn" : "pass";
19
+ }
20
+ function areaStatus(findings, prefix, fallback = "pass") {
21
+ const matches = findings.filter((finding) => finding.id.startsWith(prefix));
22
+ if (matches.some((finding) => finding.severity === "fail")) {
23
+ return "fail";
24
+ }
25
+ return matches.length > 0 ? "warn" : fallback;
26
+ }
27
+ function addFinding(findings, id, severity, message, findingPath) {
28
+ findings.push({
29
+ id,
30
+ severity,
31
+ message,
32
+ ...(findingPath ? { path: findingPath } : {})
33
+ });
34
+ }
35
+ function parseUrlTemplate(rawValue) {
36
+ if (typeof rawValue !== "string" || rawValue.length === 0 || /\s/.test(rawValue)) {
37
+ return null;
38
+ }
39
+ try {
40
+ return new URL(rawValue.replace(/\{[^{}]+\}/g, "template-value"));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function inspectUrl(findings, rawValue, findingPath, options) {
47
+ const parsed = parseUrlTemplate(rawValue);
48
+ if (!parsed || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
49
+ addFinding(findings, `${options.prefix}.url-invalid`, "fail", "URL must be an absolute HTTP or HTTPS URL.", findingPath);
50
+ return null;
51
+ }
52
+ if (parsed.username || parsed.password) {
53
+ addFinding(findings, `${options.prefix}.credentials`, "fail", "URL must not contain credentials.", findingPath);
54
+ }
55
+ if (options.httpsOnly && parsed.protocol !== "https:") {
56
+ addFinding(findings, `${options.prefix}.http`, "warn", "Public Registry URLs should use HTTPS.", findingPath);
57
+ }
58
+ return parsed;
59
+ }
60
+ function inspectInputSecrets(findings, values, findingPath) {
61
+ if (!Array.isArray(values)) {
62
+ return;
63
+ }
64
+ values.forEach((value, index) => {
65
+ if (!isRecord(value)) {
66
+ return;
67
+ }
68
+ const name = typeof value.name === "string" ? value.name : "";
69
+ if (typeof value.value === "string" && value.value.length > 0 && !/\{[^{}]+\}/.test(value.value)
70
+ && (value.isSecret === true || SECRET_NAME_PATTERN.test(name))) {
71
+ addFinding(findings, "registry.secret.embedded-value", "fail", "Secret-like Registry inputs must not embed a fixed value.", `${findingPath}[${index}].value`);
72
+ }
73
+ });
74
+ }
75
+ function inspectArguments(findings, values, findingPath) {
76
+ if (!Array.isArray(values)) {
77
+ return;
78
+ }
79
+ values.forEach((value, index) => {
80
+ if (isRecord(value) && typeof value.value === "string" && /[;&|`]/.test(value.value)) {
81
+ addFinding(findings, "registry.package.argument-shell-risk", "warn", "Fixed argument contains shell metacharacters; clients must execute without a shell.", `${findingPath}[${index}].value`);
82
+ }
83
+ });
84
+ }
85
+ function githubOwnerFromRepository(repository) {
86
+ if (repository.source !== "github" || typeof repository.url !== "string") {
87
+ return null;
88
+ }
89
+ try {
90
+ const url = new URL(repository.url);
91
+ if (url.hostname.toLowerCase() !== "github.com") {
92
+ return null;
93
+ }
94
+ return url.pathname.split("/").filter(Boolean)[0] ?? null;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ function buildInstallability(server, validPackages, validRemotes) {
101
+ const packageTypes = [...new Set(validPackages
102
+ .map((entry) => typeof entry.registryType === "string" ? entry.registryType : "")
103
+ .filter(Boolean))];
104
+ const remoteTransports = [...new Set(validRemotes
105
+ .map((entry) => typeof entry.type === "string" ? entry.type : "")
106
+ .filter(Boolean))];
107
+ const shortName = typeof server.name === "string"
108
+ ? server.name.split("/").at(-1) ?? "server"
109
+ : "server";
110
+ const remote = validRemotes.find((entry) => {
111
+ const url = parseUrlTemplate(entry.url);
112
+ return url?.protocol === "https:" && typeof entry.url === "string" && !entry.url.includes("{");
113
+ });
114
+ if (remote && typeof remote.url === "string") {
115
+ return {
116
+ codex: "ready",
117
+ packageTypes,
118
+ remoteTransports,
119
+ codexPreview: {
120
+ mcpServers: {
121
+ [shortName]: { url: remote.url }
122
+ }
123
+ }
124
+ };
125
+ }
126
+ const npmPackage = validPackages.find((entry) => entry.registryType === "npm"
127
+ && typeof entry.identifier === "string"
128
+ && typeof entry.version === "string"
129
+ && !VERSION_RANGE_PATTERN.test(entry.version)
130
+ && isRecord(entry.transport)
131
+ && entry.transport.type === "stdio");
132
+ if (npmPackage && typeof npmPackage.identifier === "string" && typeof npmPackage.version === "string") {
133
+ return {
134
+ codex: "ready",
135
+ packageTypes,
136
+ remoteTransports,
137
+ codexPreview: {
138
+ mcpServers: {
139
+ [shortName]: {
140
+ command: "npx",
141
+ args: ["-y", `${npmPackage.identifier}@${npmPackage.version}`]
142
+ }
143
+ }
144
+ }
145
+ };
146
+ }
147
+ return {
148
+ codex: validPackages.length > 0 || validRemotes.length > 0 ? "manual" : "unavailable",
149
+ packageTypes,
150
+ remoteTransports
151
+ };
152
+ }
153
+ async function readOptionalPackageJson(directory) {
154
+ try {
155
+ const parsed = JSON.parse(await readFile(path.join(directory, "package.json"), "utf8"));
156
+ return isRecord(parsed) ? parsed : null;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ async function resolveServerJsonPath(targetPath) {
163
+ const resolved = path.resolve(targetPath);
164
+ try {
165
+ return (await stat(resolved)).isDirectory() ? path.join(resolved, "server.json") : resolved;
166
+ }
167
+ catch {
168
+ return resolved.endsWith(".json") ? resolved : path.join(resolved, "server.json");
169
+ }
170
+ }
171
+ async function buildReportFromServer(server, target, source, packageJson = null, extraFindings = []) {
172
+ const findings = [...extraFindings];
173
+ const validPackages = [];
174
+ const validRemotes = [];
175
+ let ownershipVerified = false;
176
+ if (!isRecord(server)) {
177
+ addFinding(findings, "registry.metadata.object", "fail", "server.json must contain a JSON object.");
178
+ return finalizeReport({}, target, source, findings, validPackages, validRemotes, ownershipVerified);
179
+ }
180
+ if (typeof server.$schema === "string"
181
+ && OFFICIAL_SCHEMA_PATTERN.test(server.$schema)
182
+ && server.$schema !== OFFICIAL_SCHEMA) {
183
+ addFinding(findings, "registry.metadata.schema-outdated", "warn", `server.json uses an older official schema; new publications should use ${OFFICIAL_SCHEMA}.`, "$schema");
184
+ }
185
+ else if (server.$schema !== OFFICIAL_SCHEMA) {
186
+ addFinding(findings, "registry.metadata.schema", "fail", `server.json must reference the current official schema: ${OFFICIAL_SCHEMA}.`, "$schema");
187
+ }
188
+ if (typeof server.name !== "string" || server.name.length < 3
189
+ || server.name.length > 200 || !SERVER_NAME_PATTERN.test(server.name)) {
190
+ addFinding(findings, "registry.metadata.name", "fail", "Server name must use namespace/name format.", "name");
191
+ }
192
+ if (typeof server.description !== "string" || server.description.length < 1 || server.description.length > 100) {
193
+ addFinding(findings, "registry.metadata.description", "fail", "Description must contain 1 to 100 characters.", "description");
194
+ }
195
+ if (typeof server.version !== "string" || server.version.length < 1 || server.version.length > 255
196
+ || VERSION_RANGE_PATTERN.test(server.version)) {
197
+ addFinding(findings, "registry.metadata.version", "fail", "Server version must be a specific version string.", "version");
198
+ }
199
+ if (isRecord(server.repository)) {
200
+ inspectUrl(findings, server.repository.url, "repository.url", {
201
+ httpsOnly: true,
202
+ prefix: "registry.repository"
203
+ });
204
+ if (typeof server.repository.source !== "string" || server.repository.source.length === 0) {
205
+ addFinding(findings, "registry.repository.source", "fail", "Repository source is required when repository metadata is present.", "repository.source");
206
+ }
207
+ if (typeof server.name === "string" && server.name.toLowerCase().startsWith("io.github.")) {
208
+ const namespaceOwner = server.name.slice("io.github.".length).split("/")[0]?.toLowerCase();
209
+ const repositoryOwner = githubOwnerFromRepository(server.repository)?.toLowerCase();
210
+ if (repositoryOwner && namespaceOwner !== repositoryOwner) {
211
+ addFinding(findings, "registry.ownership.github-mismatch", "fail", "GitHub repository owner does not match the io.github namespace.", "repository.url");
212
+ }
213
+ }
214
+ }
215
+ if (typeof server.websiteUrl === "string") {
216
+ inspectUrl(findings, server.websiteUrl, "websiteUrl", {
217
+ httpsOnly: true,
218
+ prefix: "registry.website"
219
+ });
220
+ }
221
+ if (Array.isArray(server.icons)) {
222
+ server.icons.forEach((icon, index) => {
223
+ if (!isRecord(icon)) {
224
+ addFinding(findings, "registry.icon.invalid", "fail", "Icon entry must be an object.", `icons[${index}]`);
225
+ return;
226
+ }
227
+ inspectUrl(findings, icon.src, `icons[${index}].src`, {
228
+ httpsOnly: true,
229
+ prefix: "registry.icon"
230
+ });
231
+ });
232
+ }
233
+ if (server.packages !== undefined && !Array.isArray(server.packages)) {
234
+ addFinding(findings, "registry.package.array", "fail", "Packages must be an array.", "packages");
235
+ }
236
+ else if (Array.isArray(server.packages)) {
237
+ server.packages.forEach((entry, index) => {
238
+ const packagePath = `packages[${index}]`;
239
+ if (!isRecord(entry)) {
240
+ addFinding(findings, "registry.package.invalid", "fail", "Package entry must be an object.", packagePath);
241
+ return;
242
+ }
243
+ const type = entry.registryType;
244
+ if (typeof type !== "string" || type.length === 0) {
245
+ addFinding(findings, "registry.package.type", "fail", "Package registryType is required.", `${packagePath}.registryType`);
246
+ }
247
+ if (typeof entry.identifier !== "string" || entry.identifier.length === 0) {
248
+ addFinding(findings, "registry.package.identifier", "fail", "Package identifier is required.", `${packagePath}.identifier`);
249
+ }
250
+ else if (/^https?:\/\//i.test(entry.identifier)) {
251
+ inspectUrl(findings, entry.identifier, `${packagePath}.identifier`, {
252
+ httpsOnly: true,
253
+ prefix: "registry.package"
254
+ });
255
+ }
256
+ if (!isRecord(entry.transport) || typeof entry.transport.type !== "string"
257
+ || !["stdio", "streamable-http", "sse"].includes(entry.transport.type)) {
258
+ addFinding(findings, "registry.package.transport", "fail", "Package transport must be stdio, streamable-http, or sse.", `${packagePath}.transport`);
259
+ }
260
+ if (typeof entry.version === "string" && VERSION_RANGE_PATTERN.test(entry.version)) {
261
+ addFinding(findings, "registry.package.version-range", "fail", "Package version must be exact, not a range or latest.", `${packagePath}.version`);
262
+ }
263
+ if (type === "mcpb" && (typeof entry.fileSha256 !== "string" || !SHA256_PATTERN.test(entry.fileSha256))) {
264
+ addFinding(findings, "registry.package.mcpb-hash-missing", "fail", "MCPB packages require a lowercase SHA-256 digest.", `${packagePath}.fileSha256`);
265
+ }
266
+ inspectInputSecrets(findings, entry.environmentVariables, `${packagePath}.environmentVariables`);
267
+ inspectArguments(findings, entry.packageArguments, `${packagePath}.packageArguments`);
268
+ inspectArguments(findings, entry.runtimeArguments, `${packagePath}.runtimeArguments`);
269
+ validPackages.push(entry);
270
+ });
271
+ }
272
+ if (server.remotes !== undefined && !Array.isArray(server.remotes)) {
273
+ addFinding(findings, "registry.remote.array", "fail", "Remotes must be an array.", "remotes");
274
+ }
275
+ else if (Array.isArray(server.remotes)) {
276
+ server.remotes.forEach((entry, index) => {
277
+ const remotePath = `remotes[${index}]`;
278
+ if (!isRecord(entry)) {
279
+ addFinding(findings, "registry.remote.invalid", "fail", "Remote entry must be an object.", remotePath);
280
+ return;
281
+ }
282
+ if (entry.type !== "streamable-http" && entry.type !== "sse") {
283
+ addFinding(findings, "registry.remote.transport", "fail", "Remote transport must be streamable-http or sse.", `${remotePath}.type`);
284
+ }
285
+ inspectUrl(findings, entry.url, `${remotePath}.url`, {
286
+ httpsOnly: true,
287
+ prefix: "registry.remote"
288
+ });
289
+ inspectInputSecrets(findings, entry.headers, `${remotePath}.headers`);
290
+ validRemotes.push(entry);
291
+ });
292
+ }
293
+ if (validPackages.length === 0 && validRemotes.length === 0) {
294
+ addFinding(findings, "registry.installability.missing", "warn", "Metadata is valid, but no package or remote installation channel is declared.");
295
+ }
296
+ if (packageJson && typeof server.name === "string") {
297
+ const npmPackages = validPackages.filter((entry) => entry.registryType === "npm");
298
+ for (const entry of npmPackages) {
299
+ if (packageJson.name === entry.identifier) {
300
+ if (packageJson.mcpName !== server.name) {
301
+ addFinding(findings, "registry.ownership.npm-mcp-name", "fail", "package.json mcpName must match server.json name.", "package.json#mcpName");
302
+ }
303
+ else {
304
+ ownershipVerified = true;
305
+ }
306
+ if (typeof entry.version === "string" && packageJson.version !== entry.version) {
307
+ addFinding(findings, "registry.package.local-version-mismatch", "fail", "Local npm package version does not match server.json.", "package.json#version");
308
+ }
309
+ }
310
+ }
311
+ }
312
+ else if (validPackages.some((entry) => entry.registryType === "npm")) {
313
+ addFinding(findings, "registry.ownership.npm-unverified", "warn", "No adjacent package.json was available to verify npm mcpName ownership.");
314
+ }
315
+ return finalizeReport(server, target, source, findings, validPackages, validRemotes, ownershipVerified);
316
+ }
317
+ function finalizeReport(server, target, source, findings, validPackages, validRemotes, ownershipVerified = false) {
318
+ const status = statusFromFindings(findings);
319
+ const hasTransport = validPackages.length > 0 || validRemotes.length > 0;
320
+ const installability = buildInstallability(server, validPackages, validRemotes);
321
+ return {
322
+ schemaVersion: "1.0.0",
323
+ kind: "mcp-registry-readiness",
324
+ generatedAt: new Date().toISOString(),
325
+ source,
326
+ target,
327
+ ...(typeof server.name === "string" ? { serverName: server.name } : {}),
328
+ ...(typeof server.version === "string" ? { serverVersion: server.version } : {}),
329
+ status,
330
+ scorecard: {
331
+ metadata: areaStatus(findings, "registry.metadata."),
332
+ ownership: areaStatus(findings, "registry.ownership.", ownershipVerified ? "pass" : "skipped"),
333
+ packageIntegrity: validPackages.length > 0
334
+ ? areaStatus(findings, "registry.package.")
335
+ : "skipped",
336
+ transportReadiness: hasTransport
337
+ ? (areaStatus(findings, "registry.remote.") === "fail"
338
+ || areaStatus(findings, "registry.package.") === "fail" ? "fail" : "pass")
339
+ : "skipped",
340
+ clientInstallability: installability.codex === "ready"
341
+ ? "pass"
342
+ : installability.codex === "manual" ? "warn" : "skipped",
343
+ overall: status
344
+ },
345
+ installability,
346
+ findings
347
+ };
348
+ }
349
+ export async function buildMcpRegistryReadiness(targetPath) {
350
+ const serverJsonPath = await resolveServerJsonPath(targetPath);
351
+ let server;
352
+ try {
353
+ server = JSON.parse(await readFile(serverJsonPath, "utf8"));
354
+ }
355
+ catch {
356
+ return buildReportFromServer({}, serverJsonPath, "file", null, [{
357
+ id: "registry.metadata.read",
358
+ severity: "fail",
359
+ message: "Unable to read a valid server.json file.",
360
+ path: serverJsonPath
361
+ }]);
362
+ }
363
+ const packageJson = await readOptionalPackageJson(path.dirname(serverJsonPath));
364
+ return buildReportFromServer(server, serverJsonPath, "file", packageJson);
365
+ }
366
+ export async function inspectMcpRegistryServer(serverName, options = {}) {
367
+ if (!options.allowNetwork) {
368
+ throw new Error("Registry inspection requires explicit --allow-network consent.");
369
+ }
370
+ if (!SERVER_NAME_PATTERN.test(serverName)) {
371
+ throw new Error("Registry server name must use namespace/name format.");
372
+ }
373
+ const url = `${OFFICIAL_REGISTRY}/v0.1/servers/${encodeURIComponent(serverName)}/versions/latest`;
374
+ const response = await (options.request ?? requestBoundedHttp)(url, {
375
+ method: "GET",
376
+ headers: {
377
+ accept: "application/json",
378
+ "user-agent": "codex-plugin-doctor"
379
+ }
380
+ });
381
+ const findings = [];
382
+ if (response.statusCode !== 200) {
383
+ addFinding(findings, "registry.lookup.http", "fail", `Registry lookup returned HTTP ${response.statusCode}.`);
384
+ return buildReportFromServer({}, serverName, "registry", null, findings);
385
+ }
386
+ let payload;
387
+ try {
388
+ payload = JSON.parse(response.body.toString("utf8"));
389
+ }
390
+ catch {
391
+ addFinding(findings, "registry.lookup.json", "fail", "Registry returned invalid JSON.");
392
+ return buildReportFromServer({}, serverName, "registry", null, findings);
393
+ }
394
+ if (!isRecord(payload) || !isRecord(payload.server)) {
395
+ addFinding(findings, "registry.lookup.response", "fail", "Registry response is missing server metadata.");
396
+ return buildReportFromServer({}, serverName, "registry", null, findings);
397
+ }
398
+ if (payload.server.name !== serverName) {
399
+ addFinding(findings, "registry.lookup.name-mismatch", "fail", "Registry response server name does not match the requested exact name.");
400
+ }
401
+ const report = await buildReportFromServer(payload.server, serverName, "registry", null, findings);
402
+ const officialMeta = isRecord(payload._meta)
403
+ && isRecord(payload._meta["io.modelcontextprotocol.registry/official"])
404
+ ? payload._meta["io.modelcontextprotocol.registry/official"]
405
+ : null;
406
+ const lifecycleStatus = officialMeta && typeof officialMeta.status === "string"
407
+ ? officialMeta.status
408
+ : "unknown";
409
+ if (lifecycleStatus === "deprecated") {
410
+ addFinding(report.findings, "registry.lookup.deprecated", "warn", "Registry entry is deprecated.");
411
+ }
412
+ else if (lifecycleStatus !== "active") {
413
+ addFinding(report.findings, "registry.lookup.lifecycle", "fail", "Registry entry is not active.");
414
+ }
415
+ report.registry = {
416
+ lifecycleStatus,
417
+ ...(officialMeta && typeof officialMeta.isLatest === "boolean" ? { isLatest: officialMeta.isLatest } : {}),
418
+ ...(officialMeta && typeof officialMeta.publishedAt === "string" ? { publishedAt: officialMeta.publishedAt } : {})
419
+ };
420
+ report.status = statusFromFindings(report.findings);
421
+ report.scorecard.overall = report.status;
422
+ return report;
423
+ }
424
+ export function registryReadinessExitCode(report, requireReadiness = false) {
425
+ return report.status === "fail" || (requireReadiness && report.status !== "pass") ? 1 : 0;
426
+ }
427
+ export function renderMcpRegistryReadinessJson(report) {
428
+ return JSON.stringify(report, null, 2);
429
+ }
430
+ export function renderMcpRegistryReadiness(report) {
431
+ const lines = [
432
+ `Registry readiness: ${report.status.toUpperCase()}`,
433
+ `Source: ${report.source}`,
434
+ `Target: ${report.target}`,
435
+ report.serverName ? `Server: ${report.serverName}@${report.serverVersion ?? "unknown"}` : null,
436
+ "",
437
+ "Scorecard",
438
+ "---------",
439
+ `Metadata: ${report.scorecard.metadata}`,
440
+ `Ownership: ${report.scorecard.ownership}`,
441
+ `Package integrity: ${report.scorecard.packageIntegrity}`,
442
+ `Transport readiness: ${report.scorecard.transportReadiness}`,
443
+ `Codex installability: ${report.scorecard.clientInstallability}`,
444
+ `Overall: ${report.scorecard.overall}`,
445
+ "",
446
+ `Codex: ${report.installability.codex}`
447
+ ].filter((line) => line !== null);
448
+ if (report.findings.length > 0) {
449
+ lines.push("", "Findings", "--------");
450
+ for (const finding of report.findings) {
451
+ lines.push(`${finding.severity.toUpperCase()} ${finding.id}: ${finding.message}${finding.path ? ` (${finding.path})` : ""}`);
452
+ }
453
+ }
454
+ return lines.join("\n");
455
+ }
@@ -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: {
@@ -152,6 +214,82 @@ const publicSchemaDefinitions = [
152
214
  }
153
215
  }
154
216
  },
217
+ {
218
+ id: "doctor.registry.readiness.json",
219
+ command: "codex-plugin-doctor registry check <server.json|directory> --json",
220
+ outputKind: "mcp-registry-readiness",
221
+ required: [
222
+ "schemaVersion",
223
+ "kind",
224
+ "generatedAt",
225
+ "source",
226
+ "target",
227
+ "status",
228
+ "scorecard",
229
+ "installability",
230
+ "findings"
231
+ ],
232
+ properties: {
233
+ source: {
234
+ type: "string",
235
+ enum: ["file", "registry"]
236
+ },
237
+ status: {
238
+ type: "string",
239
+ enum: ["pass", "warn", "fail"]
240
+ },
241
+ scorecard: {
242
+ type: "object",
243
+ required: [
244
+ "metadata",
245
+ "ownership",
246
+ "packageIntegrity",
247
+ "transportReadiness",
248
+ "clientInstallability",
249
+ "overall"
250
+ ],
251
+ additionalProperties: false
252
+ },
253
+ installability: {
254
+ type: "object",
255
+ required: ["codex", "packageTypes", "remoteTransports"],
256
+ additionalProperties: true
257
+ },
258
+ findings: {
259
+ type: "array"
260
+ }
261
+ }
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
+ },
155
293
  {
156
294
  id: "doctor.audit.json",
157
295
  command: "codex-plugin-doctor audit --installed --json",
package/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
30
30
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
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
+ 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";
33
35
  export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
34
36
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditRecommendation, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
35
37
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
30
30
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
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
+ export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
34
+ export { buildMcpRegistryPublicationPreflight, registryPublicationPreflightExitCode, renderMcpRegistryPublicationPreflight, renderMcpRegistryPublicationPreflightJson } from "./core/mcp-registry-preflight.js";
33
35
  export { watchPlugin } from "./core/watch-plugin.js";
34
36
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
35
37
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";