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.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Furkan Tasci
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Furkan Tasci
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -83,18 +83,56 @@ Runtime MCP validation with `--runtime`:
83
83
 
84
84
  ### Remote MCP Readiness
85
85
 
86
- Remote MCP runtime probing is disabled until you explicitly pass `--runtime --allow-network`. Loopback endpoints also require `--allow-local-network`:
86
+ Remote MCP runtime probing is disabled until you explicitly pass `--runtime --allow-network`. Loopback endpoints also require `--allow-local-network`:
87
87
 
88
88
  ```bash
89
89
  codex-plugin-doctor check ./remote-mcp --runtime --allow-network
90
90
  codex-plugin-doctor check ./remote-mcp --runtime --allow-network --allow-local-network
91
91
  ```
92
92
 
93
- The probe makes only bounded protocol and OAuth metadata-discovery requests, redacts report output, and applies SSRF controls. It does not authenticate or follow redirects.
94
-
95
- Remote transport reliability adds one bounded SSE GET after initialization. HTTP `200` must be `text/event-stream`; HTTP `405` is compliant when the endpoint does not offer server-to-client SSE. At most one SSE resume and one session restart are attempted, and no remote response content is retained. This is a bounded readiness check, not a live interoperability, delivery-guarantee, or load-test claim.
96
-
97
- `--allow-session-lifecycle` is disabled by default and is state-changing: only after a valid `MCP-Session-Id` it permits one bounded session `DELETE`. `--require-remote-reliability` is a strict result gate: it fails unless every attempted remote reliability scorecard passes. Local-only runs are unaffected. It grants no network consent, so `--runtime --allow-network` (and loopback consent when applicable) remain required. See [Remote MCP Readiness](./docs/architecture/remote-mcp-readiness.md) and [Remote MCP Transport Reliability](./docs/architecture/remote-mcp-transport-reliability.md).
93
+ The probe makes only bounded protocol and OAuth metadata-discovery requests, redacts report output, and applies SSRF controls. It does not authenticate or follow redirects.
94
+
95
+ Remote transport reliability adds one bounded SSE GET after initialization. HTTP `200` must be `text/event-stream`; HTTP `405` is compliant when the endpoint does not offer server-to-client SSE. At most one SSE resume and one session restart are attempted, and no remote response content is retained. This is a bounded readiness check, not a live interoperability, delivery-guarantee, or load-test claim.
96
+
97
+ `--allow-session-lifecycle` is disabled by default and is state-changing: only after a valid `MCP-Session-Id` it permits one bounded session `DELETE`. `--require-remote-reliability` is a strict result gate: it fails unless every attempted remote reliability scorecard passes. Local-only runs are unaffected. It grants no network consent, so `--runtime --allow-network` (and loopback consent when applicable) remain required. See [Remote MCP Readiness](./docs/architecture/remote-mcp-readiness.md) and [Remote MCP Transport Reliability](./docs/architecture/remote-mcp-transport-reliability.md).
98
+
99
+ ### MCP Registry Readiness
100
+
101
+ Validate official MCP Registry publication metadata without publishing or installing anything:
102
+
103
+ ```bash
104
+ codex-plugin-doctor registry check path/to/server.json
105
+ codex-plugin-doctor registry check path/to/server.json --json --require-registry-readiness
106
+ codex-plugin-doctor registry inspect io.github.example/weather --allow-network
107
+ ```
108
+
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
+
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).
98
136
 
99
137
  Output formats:
100
138
 
@@ -442,9 +480,9 @@ jobs:
442
480
  runs-on: ubuntu-latest
443
481
  steps:
444
482
  - uses: actions/checkout@v5
445
- - uses: Esquetta/CodexPluginDoctor@v1.53.0
483
+ - uses: Esquetta/CodexPluginDoctor@v1.55.0
446
484
  with:
447
- version: "1.53.0"
485
+ version: "1.55.0"
448
486
  path: .
449
487
  runtime: "true"
450
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
+ }
@@ -0,0 +1,57 @@
1
+ import { type BoundedHttpRequestOptions, type BoundedHttpResponse } from "./bounded-http-client.js";
2
+ type RegistryCheckStatus = "pass" | "warn" | "fail" | "skipped";
3
+ type RegistryRequest = (url: string, options?: BoundedHttpRequestOptions) => Promise<BoundedHttpResponse>;
4
+ export interface McpRegistryFinding {
5
+ id: string;
6
+ severity: "warn" | "fail";
7
+ message: string;
8
+ path?: string;
9
+ }
10
+ export interface McpRegistryScorecard {
11
+ metadata: RegistryCheckStatus;
12
+ ownership: RegistryCheckStatus;
13
+ packageIntegrity: RegistryCheckStatus;
14
+ transportReadiness: RegistryCheckStatus;
15
+ clientInstallability: RegistryCheckStatus;
16
+ overall: Exclude<RegistryCheckStatus, "skipped">;
17
+ }
18
+ export interface McpRegistryInstallability {
19
+ codex: "ready" | "manual" | "unavailable";
20
+ packageTypes: string[];
21
+ remoteTransports: string[];
22
+ codexPreview?: {
23
+ mcpServers: Record<string, {
24
+ command?: string;
25
+ args?: string[];
26
+ url?: string;
27
+ }>;
28
+ };
29
+ }
30
+ export interface McpRegistryReadinessReport {
31
+ schemaVersion: "1.0.0";
32
+ kind: "mcp-registry-readiness";
33
+ generatedAt: string;
34
+ source: "file" | "registry";
35
+ target: string;
36
+ serverName?: string;
37
+ serverVersion?: string;
38
+ status: "pass" | "warn" | "fail";
39
+ scorecard: McpRegistryScorecard;
40
+ installability: McpRegistryInstallability;
41
+ findings: McpRegistryFinding[];
42
+ registry?: {
43
+ lifecycleStatus: string;
44
+ isLatest?: boolean;
45
+ publishedAt?: string;
46
+ };
47
+ }
48
+ export interface InspectMcpRegistryOptions {
49
+ allowNetwork?: boolean;
50
+ request?: RegistryRequest;
51
+ }
52
+ export declare function buildMcpRegistryReadiness(targetPath: string): Promise<McpRegistryReadinessReport>;
53
+ export declare function inspectMcpRegistryServer(serverName: string, options?: InspectMcpRegistryOptions): Promise<McpRegistryReadinessReport>;
54
+ export declare function registryReadinessExitCode(report: McpRegistryReadinessReport, requireReadiness?: boolean): 0 | 1;
55
+ export declare function renderMcpRegistryReadinessJson(report: McpRegistryReadinessReport): string;
56
+ export declare function renderMcpRegistryReadiness(report: McpRegistryReadinessReport): string;
57
+ export {};