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 +21 -21
- package/README.md +46 -8
- package/dist/core/mcp-registry-preflight.d.ts +38 -0
- package/dist/core/mcp-registry-preflight.js +358 -0
- package/dist/core/mcp-registry.d.ts +57 -0
- package/dist/core/mcp-registry.js +455 -0
- package/dist/core/output-contract.js +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/run-cli.js +90 -0
- package/examples/README.md +94 -94
- package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
- package/examples/codex-doctor-generic-mcp/server.js +1 -1
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-risky/.mcp.json +12 -12
- package/examples/codex-doctor-risky/mock-server.js +1 -1
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
- package/examples/codex-doctor-runtime/.mcp.json +9 -9
- package/examples/codex-doctor-runtime/mock-server.js +237 -237
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
- package/package.json +1 -1
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.
|
|
483
|
+
- uses: Esquetta/CodexPluginDoctor@v1.55.0
|
|
446
484
|
with:
|
|
447
|
-
version: "1.
|
|
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 {};
|