codex-plugin-doctor 1.52.0 → 1.53.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 +19 -15
- package/dist/core/bounded-http-client.js +2 -1
- package/dist/core/doctor-export-bundle.js +3 -1
- package/dist/core/output-contract.js +27 -0
- package/dist/core/release-check.d.ts +2 -0
- package/dist/core/release-check.js +2 -0
- package/dist/core/release-evidence.d.ts +2 -0
- package/dist/core/release-evidence.js +2 -0
- package/dist/core/remote-mcp-probe.d.ts +1 -0
- package/dist/core/remote-mcp-probe.js +59 -1
- package/dist/core/remote-transport-reliability.d.ts +18 -0
- package/dist/core/remote-transport-reliability.js +243 -0
- package/dist/core/runtime-probe.d.ts +3 -1
- package/dist/core/runtime-probe.js +18 -1
- package/dist/core/sse-observation.d.ts +7 -0
- package/dist/core/sse-observation.js +67 -0
- package/dist/core/validate-plugin.js +4 -3
- package/dist/domain/types.d.ts +13 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +2 -0
- package/dist/mcp/generic-mcp-doctor.js +6 -3
- package/dist/reporting/render-markdown-report.js +13 -0
- package/dist/reporting/render-text-report.js +11 -0
- package/dist/rules/rule-catalog.js +108 -0
- package/dist/run-cli.js +81 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,21 +78,25 @@ Runtime MCP validation with `--runtime`:
|
|
|
78
78
|
- paginated list responses
|
|
79
79
|
- runtime capability scorecard
|
|
80
80
|
- redacted verbose transcript with `--verbose-runtime`
|
|
81
|
-
- optional runtime approval gating with a precomputed `doctor runtime-plan` digest
|
|
82
|
-
- optional Docker isolation for local Node.js stdio servers with `--sandbox docker`
|
|
83
|
-
|
|
84
|
-
### Remote MCP Readiness
|
|
85
|
-
|
|
86
|
-
Remote MCP runtime probing is disabled until you explicitly pass `--allow-network`.
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
codex-plugin-doctor check ./remote-mcp --runtime --allow-network
|
|
90
|
-
codex-plugin-doctor check ./remote-mcp --runtime --allow-network --allow-local-network
|
|
91
|
-
```
|
|
81
|
+
- optional runtime approval gating with a precomputed `doctor runtime-plan` digest
|
|
82
|
+
- optional Docker isolation for local Node.js stdio servers with `--sandbox docker`
|
|
83
|
+
|
|
84
|
+
### Remote MCP Readiness
|
|
85
|
+
|
|
86
|
+
Remote MCP runtime probing is disabled until you explicitly pass `--runtime --allow-network`. Loopback endpoints also require `--allow-local-network`:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
codex-plugin-doctor check ./remote-mcp --runtime --allow-network
|
|
90
|
+
codex-plugin-doctor check ./remote-mcp --runtime --allow-network --allow-local-network
|
|
91
|
+
```
|
|
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.
|
|
92
94
|
|
|
93
|
-
|
|
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.
|
|
94
96
|
|
|
95
|
-
|
|
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
|
+
Output formats:
|
|
96
100
|
|
|
97
101
|
- human text output
|
|
98
102
|
- JSON reports
|
|
@@ -438,9 +442,9 @@ jobs:
|
|
|
438
442
|
runs-on: ubuntu-latest
|
|
439
443
|
steps:
|
|
440
444
|
- uses: actions/checkout@v5
|
|
441
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
445
|
+
- uses: Esquetta/CodexPluginDoctor@v1.53.0
|
|
442
446
|
with:
|
|
443
|
-
version: "1.
|
|
447
|
+
version: "1.53.0"
|
|
444
448
|
path: .
|
|
445
449
|
runtime: "true"
|
|
446
450
|
policy: codex-publish
|
|
@@ -7,6 +7,7 @@ const DEFAULT_MAX_RESPONSE_BYTES = 1_024 * 1_024;
|
|
|
7
7
|
const allowedRequestHeaders = new Set([
|
|
8
8
|
"accept",
|
|
9
9
|
"content-type",
|
|
10
|
+
"last-event-id",
|
|
10
11
|
"mcp-protocol-version",
|
|
11
12
|
"mcp-session-id",
|
|
12
13
|
"user-agent"
|
|
@@ -137,7 +138,7 @@ export async function requestBoundedHttp(rawUrl, options = {}) {
|
|
|
137
138
|
let response;
|
|
138
139
|
let settled = false;
|
|
139
140
|
const timeout = setTimeout(() => {
|
|
140
|
-
fail(new BoundedHttpError("REMOTE_HTTP_TIMEOUT", "Remote HTTP request timed out."));
|
|
141
|
+
fail(new BoundedHttpError("REMOTE_HTTP_TIMEOUT", "Remote HTTP request timed out.", response?.statusCode, response ? selectSafeHeaders(response.headers) : undefined));
|
|
141
142
|
}, remainingMs);
|
|
142
143
|
const fail = (error) => {
|
|
143
144
|
if (settled) {
|
|
@@ -16,7 +16,9 @@ export function redactValue(value) {
|
|
|
16
16
|
if (typeof value === "object" && value !== null) {
|
|
17
17
|
return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
|
|
18
18
|
key,
|
|
19
|
-
|
|
19
|
+
/^(?:mcp[-_]?session[-_]?id|session[-_]?id|last[-_]?event[-_]?id|event[-_]?id|retry|(?:sse[-_]?)?data)$/i.test(key)
|
|
20
|
+
? "[REDACTED_SECRET]"
|
|
21
|
+
: redactValue(nestedValue)
|
|
20
22
|
]));
|
|
21
23
|
}
|
|
22
24
|
return value;
|
|
@@ -5,6 +5,10 @@ const runtimeCapabilityStatusSchema = {
|
|
|
5
5
|
type: "string",
|
|
6
6
|
enum: ["pass", "fail", "warn", "skipped", "unsupported"]
|
|
7
7
|
};
|
|
8
|
+
const remoteTransportReliabilityStatusSchema = {
|
|
9
|
+
type: "string",
|
|
10
|
+
enum: ["pass", "warn", "fail", "skipped"]
|
|
11
|
+
};
|
|
8
12
|
const runtimeConformanceSchema = {
|
|
9
13
|
type: "object",
|
|
10
14
|
properties: {
|
|
@@ -35,6 +39,28 @@ const runtimeConformanceSchema = {
|
|
|
35
39
|
],
|
|
36
40
|
additionalProperties: false
|
|
37
41
|
};
|
|
42
|
+
const remoteTransportReliabilitySchema = {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
getSse: remoteTransportReliabilityStatusSchema,
|
|
46
|
+
sessionPropagation: remoteTransportReliabilityStatusSchema,
|
|
47
|
+
resumability: remoteTransportReliabilityStatusSchema,
|
|
48
|
+
disconnectSafety: remoteTransportReliabilityStatusSchema,
|
|
49
|
+
sessionRestart: remoteTransportReliabilityStatusSchema,
|
|
50
|
+
termination: remoteTransportReliabilityStatusSchema,
|
|
51
|
+
overall: remoteTransportReliabilityStatusSchema
|
|
52
|
+
},
|
|
53
|
+
required: [
|
|
54
|
+
"getSse",
|
|
55
|
+
"sessionPropagation",
|
|
56
|
+
"resumability",
|
|
57
|
+
"disconnectSafety",
|
|
58
|
+
"sessionRestart",
|
|
59
|
+
"termination",
|
|
60
|
+
"overall"
|
|
61
|
+
],
|
|
62
|
+
additionalProperties: false
|
|
63
|
+
};
|
|
38
64
|
const remoteRuntimeScorecardSchema = {
|
|
39
65
|
type: "object",
|
|
40
66
|
properties: {
|
|
@@ -48,6 +74,7 @@ const remoteRuntimeScorecardSchema = {
|
|
|
48
74
|
},
|
|
49
75
|
protocolHeaders: runtimeCapabilityStatusSchema,
|
|
50
76
|
authorization: runtimeCapabilityStatusSchema,
|
|
77
|
+
reliability: remoteTransportReliabilitySchema,
|
|
51
78
|
overall: {
|
|
52
79
|
type: "string",
|
|
53
80
|
enum: ["pass", "warn", "fail", "skipped"]
|
|
@@ -43,6 +43,8 @@ export interface BuildReleaseCheckOptions {
|
|
|
43
43
|
runtime?: boolean;
|
|
44
44
|
allowNetwork?: boolean;
|
|
45
45
|
allowLocalNetwork?: boolean;
|
|
46
|
+
allowSessionLifecycle?: boolean;
|
|
47
|
+
requireRemoteReliability?: boolean;
|
|
46
48
|
runtimeSandbox?: RuntimeSandboxMode;
|
|
47
49
|
runCheck?: (targetPath: string, options: CheckOptions) => Promise<CheckResult>;
|
|
48
50
|
}
|
|
@@ -108,6 +108,8 @@ export async function buildReleaseCheck(targetPath, options = {}) {
|
|
|
108
108
|
runtime: runtimeProbeEnabled,
|
|
109
109
|
...(options.allowNetwork ? { allowNetwork: true } : {}),
|
|
110
110
|
...(options.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
111
|
+
...(options.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
112
|
+
...(options.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
111
113
|
...(options.runtimeSandbox ? { runtimeSandbox: options.runtimeSandbox } : {})
|
|
112
114
|
});
|
|
113
115
|
const securityResult = await buildSecurityAudit(resolvedPath);
|
|
@@ -113,6 +113,8 @@ export interface BuildDoctorReleaseEvidenceOptions {
|
|
|
113
113
|
runtime?: boolean;
|
|
114
114
|
allowNetwork?: boolean;
|
|
115
115
|
allowLocalNetwork?: boolean;
|
|
116
|
+
allowSessionLifecycle?: boolean;
|
|
117
|
+
requireRemoteReliability?: boolean;
|
|
116
118
|
sandbox?: RuntimeSandboxMode;
|
|
117
119
|
environment?: CompatibilityEnvironment;
|
|
118
120
|
runCheck?: (targetPath: string, options?: CheckOptions) => Promise<CheckResult>;
|
|
@@ -308,6 +308,8 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
|
|
|
308
308
|
runtime: true,
|
|
309
309
|
...(options.allowNetwork ? { allowNetwork: true } : {}),
|
|
310
310
|
...(options.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
311
|
+
...(options.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
312
|
+
...(options.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
311
313
|
...(options.sandbox ? { runtimeSandbox: options.sandbox } : {})
|
|
312
314
|
}
|
|
313
315
|
: {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BoundedHttpError, requestBoundedHttp } from "./bounded-http-client.js";
|
|
2
2
|
import { RemoteNetworkPolicyError } from "./remote-network-policy.js";
|
|
3
3
|
import { checkRemoteOAuthReadiness } from "./remote-oauth-readiness.js";
|
|
4
|
+
import { probeRemoteTransportReliability } from "./remote-transport-reliability.js";
|
|
4
5
|
import { inspectRemoteMcpUrl } from "./remote-url-policy.js";
|
|
5
6
|
import { packageVersion } from "../version.js";
|
|
6
7
|
const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
@@ -85,6 +86,9 @@ function isValidInitializeResponse(message) {
|
|
|
85
86
|
typeof message.result.serverInfo.name === "string" &&
|
|
86
87
|
typeof message.result.serverInfo.version === "string");
|
|
87
88
|
}
|
|
89
|
+
function isInitializedNotificationAcknowledged(response) {
|
|
90
|
+
return response.statusCode === 202 && response.body.length === 0;
|
|
91
|
+
}
|
|
88
92
|
function validSessionId(value) {
|
|
89
93
|
return value !== null && /^[\x21-\x7e]+$/.test(value);
|
|
90
94
|
}
|
|
@@ -210,7 +214,7 @@ export async function probeRemoteMcpServer(serverName, rawUrl, options = {}) {
|
|
|
210
214
|
...(sessionId === null ? {} : { "MCP-Session-Id": sessionId })
|
|
211
215
|
}
|
|
212
216
|
});
|
|
213
|
-
if (initializedResponse
|
|
217
|
+
if (!isInitializedNotificationAcknowledged(initializedResponse)) {
|
|
214
218
|
scorecard.protocolHeaders = "fail";
|
|
215
219
|
findings.push(failure("plugin.runtime.remote.initialized.failed", `The remote MCP server \`${serverName}\` did not acknowledge the initialized notification.`, "The MCP session may not be ready for subsequent protocol traffic.", "Accept a successful HTTP response to notifications/initialized at the same MCP endpoint."));
|
|
216
220
|
return finalize(scorecard, findings);
|
|
@@ -222,5 +226,59 @@ export async function probeRemoteMcpServer(serverName, rawUrl, options = {}) {
|
|
|
222
226
|
return finalize(scorecard, findings);
|
|
223
227
|
}
|
|
224
228
|
scorecard.protocolHeaders = "pass";
|
|
229
|
+
const reliability = await probeRemoteTransportReliability({
|
|
230
|
+
rawUrl,
|
|
231
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
232
|
+
request,
|
|
233
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
234
|
+
requestOptions: {
|
|
235
|
+
allowLocalNetwork: options.allowLocalNetwork,
|
|
236
|
+
lookup: options.lookup
|
|
237
|
+
},
|
|
238
|
+
sessionId,
|
|
239
|
+
allowSessionLifecycle: options.allowSessionLifecycle,
|
|
240
|
+
reinitialize: async (requestWithinBudget) => {
|
|
241
|
+
const restartInitialize = await requestWithinBudget({
|
|
242
|
+
method: "POST",
|
|
243
|
+
body: initializeBody,
|
|
244
|
+
headers: {
|
|
245
|
+
Accept: "application/json, text/event-stream",
|
|
246
|
+
"Content-Type": "application/json"
|
|
247
|
+
},
|
|
248
|
+
stopAfter: (body) => findSseInitializeResponse(body) !== null
|
|
249
|
+
});
|
|
250
|
+
if (restartInitialize.statusCode !== 200) {
|
|
251
|
+
throw new Error("restart initialize failed");
|
|
252
|
+
}
|
|
253
|
+
const restartContentType = mediaType(responseHeader(restartInitialize.headers, "content-type"));
|
|
254
|
+
if (restartContentType !== "application/json" && restartContentType !== "text/event-stream") {
|
|
255
|
+
throw new Error("restart initialize content type invalid");
|
|
256
|
+
}
|
|
257
|
+
const replacementSessionId = responseHeader(restartInitialize.headers, "mcp-session-id");
|
|
258
|
+
if (replacementSessionId !== null && !validSessionId(replacementSessionId)) {
|
|
259
|
+
throw new Error("restart session invalid");
|
|
260
|
+
}
|
|
261
|
+
const restartMessage = parseInitializeResponse(restartInitialize.body, restartContentType);
|
|
262
|
+
if (!restartMessage || !isValidInitializeResponse(restartMessage)) {
|
|
263
|
+
throw new Error("restart initialize result invalid");
|
|
264
|
+
}
|
|
265
|
+
const restartInitialized = await requestWithinBudget({
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
268
|
+
headers: {
|
|
269
|
+
Accept: "application/json, text/event-stream",
|
|
270
|
+
"Content-Type": "application/json",
|
|
271
|
+
"MCP-Protocol-Version": MCP_PROTOCOL_VERSION,
|
|
272
|
+
...(replacementSessionId === null ? {} : { "MCP-Session-Id": replacementSessionId })
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
if (!isInitializedNotificationAcknowledged(restartInitialized)) {
|
|
276
|
+
throw new Error("restart initialized failed");
|
|
277
|
+
}
|
|
278
|
+
return replacementSessionId;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
scorecard.reliability = reliability.scorecard;
|
|
282
|
+
findings.push(...reliability.findings);
|
|
225
283
|
return finalize(scorecard, findings);
|
|
226
284
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type BoundedHttpRequestOptions, type BoundedHttpResponse } from "./bounded-http-client.js";
|
|
2
|
+
import type { Finding, RemoteTransportReliabilityScorecard } from "../domain/types.js";
|
|
3
|
+
export interface RemoteTransportReliabilityResult {
|
|
4
|
+
findings: Finding[];
|
|
5
|
+
scorecard: RemoteTransportReliabilityScorecard;
|
|
6
|
+
}
|
|
7
|
+
export type RemoteTransportReliabilityRequest = (options: BoundedHttpRequestOptions) => Promise<BoundedHttpResponse>;
|
|
8
|
+
export interface RemoteTransportReliabilityOptions {
|
|
9
|
+
rawUrl: string;
|
|
10
|
+
protocolVersion: string;
|
|
11
|
+
request: (rawUrl: string, options?: BoundedHttpRequestOptions) => Promise<BoundedHttpResponse>;
|
|
12
|
+
requestTimeoutMs?: number;
|
|
13
|
+
requestOptions?: Pick<BoundedHttpRequestOptions, "allowLocalNetwork" | "lookup">;
|
|
14
|
+
sessionId: string | null;
|
|
15
|
+
allowSessionLifecycle?: boolean;
|
|
16
|
+
reinitialize: (request: RemoteTransportReliabilityRequest) => Promise<string | null>;
|
|
17
|
+
}
|
|
18
|
+
export declare function probeRemoteTransportReliability(options: RemoteTransportReliabilityOptions): Promise<RemoteTransportReliabilityResult>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { BoundedHttpError } from "./bounded-http-client.js";
|
|
2
|
+
import { observeFirstSseEvent } from "./sse-observation.js";
|
|
3
|
+
const DEFAULT_RELIABILITY_TIMEOUT_MS = 3_000;
|
|
4
|
+
const MAX_RELIABILITY_REQUESTS = 6;
|
|
5
|
+
function createScorecard() {
|
|
6
|
+
return {
|
|
7
|
+
getSse: "skipped",
|
|
8
|
+
sessionPropagation: "skipped",
|
|
9
|
+
resumability: "skipped",
|
|
10
|
+
disconnectSafety: "skipped",
|
|
11
|
+
sessionRestart: "skipped",
|
|
12
|
+
termination: "skipped",
|
|
13
|
+
overall: "skipped"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function finding(id, severity, message, suggestedFix) {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
severity,
|
|
20
|
+
message,
|
|
21
|
+
impact: "Remote MCP transport reliability could not be verified within the configured safety bounds.",
|
|
22
|
+
suggestedFix
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function responseHeader(headers, name) {
|
|
26
|
+
const value = headers[name];
|
|
27
|
+
return typeof value === "string" ? value : null;
|
|
28
|
+
}
|
|
29
|
+
function mediaType(value) {
|
|
30
|
+
return value?.split(";", 1)[0]?.trim().toLowerCase() ?? null;
|
|
31
|
+
}
|
|
32
|
+
function validSessionId(value) {
|
|
33
|
+
return value !== null && /^[\x21-\x7e]+$/.test(value);
|
|
34
|
+
}
|
|
35
|
+
function wait(delayMs) {
|
|
36
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
37
|
+
}
|
|
38
|
+
function finalize(scorecard, findings) {
|
|
39
|
+
const statuses = Object.entries(scorecard)
|
|
40
|
+
.filter(([name]) => name !== "overall")
|
|
41
|
+
.map(([, status]) => status);
|
|
42
|
+
scorecard.overall = statuses.includes("fail")
|
|
43
|
+
? "fail"
|
|
44
|
+
: statuses.includes("warn")
|
|
45
|
+
? "warn"
|
|
46
|
+
: statuses.includes("pass")
|
|
47
|
+
? "pass"
|
|
48
|
+
: "skipped";
|
|
49
|
+
return { findings, scorecard };
|
|
50
|
+
}
|
|
51
|
+
function isSseTimeout(error) {
|
|
52
|
+
return error instanceof BoundedHttpError
|
|
53
|
+
&& error.code === "REMOTE_HTTP_TIMEOUT"
|
|
54
|
+
&& error.statusCode === 200
|
|
55
|
+
&& mediaType(responseHeader(error.headers ?? {}, "content-type")) === "text/event-stream";
|
|
56
|
+
}
|
|
57
|
+
function classifySseResponse(response, failurePrefix, findings) {
|
|
58
|
+
if (response.statusCode !== 200) {
|
|
59
|
+
findings.push(finding(`plugin.runtime.remote.reliability.${failurePrefix}.status`, "fail", "The remote MCP endpoint returned an unsupported SSE transport status.", "Return HTTP 200 for an accepted SSE transport request."));
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (mediaType(responseHeader(response.headers, "content-type")) !== "text/event-stream") {
|
|
63
|
+
findings.push(finding(`plugin.runtime.remote.reliability.${failurePrefix}.content_type`, "fail", "The remote MCP endpoint returned a non-SSE media type for an SSE transport request.", "Return text/event-stream for accepted SSE transport requests."));
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return observeFirstSseEvent(response.body);
|
|
67
|
+
}
|
|
68
|
+
export async function probeRemoteTransportReliability(options) {
|
|
69
|
+
const scorecard = createScorecard();
|
|
70
|
+
const findings = [];
|
|
71
|
+
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_RELIABILITY_TIMEOUT_MS;
|
|
72
|
+
const deadline = Date.now() + Math.min(timeoutMs, DEFAULT_RELIABILITY_TIMEOUT_MS);
|
|
73
|
+
let currentSessionId = options.sessionId;
|
|
74
|
+
let requestCount = 0;
|
|
75
|
+
let restarted = false;
|
|
76
|
+
const requestWithinBudget = async (requestOptions) => {
|
|
77
|
+
if (requestCount >= MAX_RELIABILITY_REQUESTS) {
|
|
78
|
+
throw new Error("reliability request ceiling reached");
|
|
79
|
+
}
|
|
80
|
+
const remainingMs = deadline - Date.now();
|
|
81
|
+
if (remainingMs <= 0) {
|
|
82
|
+
throw new BoundedHttpError("REMOTE_HTTP_TIMEOUT", "Remote reliability deadline elapsed.");
|
|
83
|
+
}
|
|
84
|
+
requestCount += 1;
|
|
85
|
+
return options.request(options.rawUrl, {
|
|
86
|
+
...requestOptions,
|
|
87
|
+
...options.requestOptions,
|
|
88
|
+
timeoutMs: Math.max(1, Math.min(remainingMs, DEFAULT_RELIABILITY_TIMEOUT_MS))
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const send = async (method, extraHeaders = {}, stopAfter) => {
|
|
92
|
+
let sessionRestarted = false;
|
|
93
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
94
|
+
const sessionId = currentSessionId;
|
|
95
|
+
const headers = {
|
|
96
|
+
Accept: "text/event-stream",
|
|
97
|
+
"MCP-Protocol-Version": options.protocolVersion,
|
|
98
|
+
...(sessionId === null ? {} : { "MCP-Session-Id": sessionId }),
|
|
99
|
+
...(sessionRestarted ? {} : extraHeaders)
|
|
100
|
+
};
|
|
101
|
+
if (sessionId !== null) {
|
|
102
|
+
scorecard.sessionPropagation = "pass";
|
|
103
|
+
}
|
|
104
|
+
let response;
|
|
105
|
+
try {
|
|
106
|
+
response = await requestWithinBudget({
|
|
107
|
+
method,
|
|
108
|
+
headers,
|
|
109
|
+
stopAfter
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return { error };
|
|
114
|
+
}
|
|
115
|
+
if (method === "GET" && sessionId !== null && response.statusCode === 404) {
|
|
116
|
+
if (restarted) {
|
|
117
|
+
scorecard.sessionRestart = "fail";
|
|
118
|
+
return { restartFailed: true };
|
|
119
|
+
}
|
|
120
|
+
restarted = true;
|
|
121
|
+
sessionRestarted = true;
|
|
122
|
+
currentSessionId = null;
|
|
123
|
+
try {
|
|
124
|
+
const replacementSessionId = await options.reinitialize(requestWithinBudget);
|
|
125
|
+
if (replacementSessionId !== null && !validSessionId(replacementSessionId)) {
|
|
126
|
+
throw new Error("invalid replacement session");
|
|
127
|
+
}
|
|
128
|
+
currentSessionId = replacementSessionId;
|
|
129
|
+
scorecard.sessionRestart = "pass";
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
scorecard.sessionRestart = "fail";
|
|
133
|
+
return { restartFailed: true };
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
return { response, sessionRestarted };
|
|
138
|
+
}
|
|
139
|
+
return { restartFailed: true };
|
|
140
|
+
};
|
|
141
|
+
const first = await send("GET", {}, (body) => observeFirstSseEvent(body).complete);
|
|
142
|
+
if (first.restartFailed) {
|
|
143
|
+
findings.push(finding("plugin.runtime.remote.reliability.session_restart.failed", "fail", "The remote MCP session expired and could not be restarted within the bounded probe.", "Accept a fresh initialize sequence after an expired MCP session."));
|
|
144
|
+
return finalize(scorecard, findings);
|
|
145
|
+
}
|
|
146
|
+
if (first.error) {
|
|
147
|
+
if (isSseTimeout(first.error)) {
|
|
148
|
+
scorecard.getSse = "warn";
|
|
149
|
+
scorecard.disconnectSafety = "warn";
|
|
150
|
+
findings.push(finding("plugin.runtime.remote.reliability.get.inconclusive", "warn", "The remote MCP SSE stream did not produce a complete event before the bounded observation deadline.", "Emit complete SSE event frames promptly when server-to-client streaming is supported."));
|
|
151
|
+
return finalize(scorecard, findings);
|
|
152
|
+
}
|
|
153
|
+
scorecard.getSse = "fail";
|
|
154
|
+
findings.push(finding("plugin.runtime.remote.reliability.get.failed", "fail", "The remote MCP SSE transport request could not be completed.", "Keep the Streamable HTTP transport reachable within the configured request bounds."));
|
|
155
|
+
return finalize(scorecard, findings);
|
|
156
|
+
}
|
|
157
|
+
const firstResponse = first.response;
|
|
158
|
+
if (!firstResponse) {
|
|
159
|
+
scorecard.getSse = "fail";
|
|
160
|
+
return finalize(scorecard, findings);
|
|
161
|
+
}
|
|
162
|
+
if (firstResponse.statusCode === 405) {
|
|
163
|
+
scorecard.getSse = "pass";
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const observation = classifySseResponse(firstResponse, "get", findings);
|
|
167
|
+
if (observation === null) {
|
|
168
|
+
scorecard.getSse = "fail";
|
|
169
|
+
return finalize(scorecard, findings);
|
|
170
|
+
}
|
|
171
|
+
if (!observation.complete) {
|
|
172
|
+
scorecard.getSse = "warn";
|
|
173
|
+
scorecard.disconnectSafety = "warn";
|
|
174
|
+
findings.push(finding("plugin.runtime.remote.reliability.get.inconclusive", "warn", "The remote MCP SSE response ended before a complete event could be observed.", "Emit complete SSE event frames when server-to-client streaming is supported."));
|
|
175
|
+
return finalize(scorecard, findings);
|
|
176
|
+
}
|
|
177
|
+
if (observation.malformed) {
|
|
178
|
+
scorecard.getSse = "fail";
|
|
179
|
+
scorecard.disconnectSafety = "fail";
|
|
180
|
+
findings.push(finding("plugin.runtime.remote.reliability.get.malformed", "fail", "The remote MCP endpoint emitted malformed SSE event framing.", "Return complete SSE events with valid id and retry fields."));
|
|
181
|
+
return finalize(scorecard, findings);
|
|
182
|
+
}
|
|
183
|
+
scorecard.getSse = "pass";
|
|
184
|
+
scorecard.disconnectSafety = "pass";
|
|
185
|
+
if (observation.eventId !== null) {
|
|
186
|
+
const remainingMs = deadline - Date.now();
|
|
187
|
+
if (observation.retryMs !== null) {
|
|
188
|
+
if (observation.retryMs >= remainingMs) {
|
|
189
|
+
scorecard.resumability = "warn";
|
|
190
|
+
findings.push(finding("plugin.runtime.remote.reliability.resume.inconclusive", "warn", "The remote MCP SSE retry delay exceeded the remaining bounded observation deadline.", "Advertise an SSE retry delay that fits within the configured probe deadline."));
|
|
191
|
+
return finalize(scorecard, findings);
|
|
192
|
+
}
|
|
193
|
+
await wait(observation.retryMs);
|
|
194
|
+
}
|
|
195
|
+
const resumed = await send("GET", { "Last-Event-ID": observation.eventId }, (body) => observeFirstSseEvent(body).complete);
|
|
196
|
+
if (resumed.restartFailed) {
|
|
197
|
+
findings.push(finding("plugin.runtime.remote.reliability.session_restart.failed", "fail", "The remote MCP session expired during SSE resumability validation and could not be restarted.", "Accept one fresh initialize sequence after an expired MCP session."));
|
|
198
|
+
return finalize(scorecard, findings);
|
|
199
|
+
}
|
|
200
|
+
if (resumed.error) {
|
|
201
|
+
scorecard.resumability = isSseTimeout(resumed.error) ? "warn" : "fail";
|
|
202
|
+
findings.push(finding(isSseTimeout(resumed.error)
|
|
203
|
+
? "plugin.runtime.remote.reliability.resume.inconclusive"
|
|
204
|
+
: "plugin.runtime.remote.reliability.resume.failed", isSseTimeout(resumed.error) ? "warn" : "fail", "The remote MCP SSE resume request could not be completed within the bounded probe.", "Accept one bounded SSE reconnect using Last-Event-ID."));
|
|
205
|
+
return finalize(scorecard, findings);
|
|
206
|
+
}
|
|
207
|
+
if (!resumed.response) {
|
|
208
|
+
scorecard.resumability = "fail";
|
|
209
|
+
return finalize(scorecard, findings);
|
|
210
|
+
}
|
|
211
|
+
if (resumed.sessionRestarted) {
|
|
212
|
+
scorecard.resumability = "skipped";
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
const resumeObservation = classifySseResponse(resumed.response, "resume", findings);
|
|
216
|
+
if (resumeObservation === null || resumeObservation.malformed) {
|
|
217
|
+
scorecard.resumability = "fail";
|
|
218
|
+
if (resumeObservation?.malformed) {
|
|
219
|
+
findings.push(finding("plugin.runtime.remote.reliability.resume.malformed", "fail", "The remote MCP endpoint emitted malformed SSE framing after a resume request.", "Return complete SSE events with valid id and retry fields after reconnecting."));
|
|
220
|
+
}
|
|
221
|
+
return finalize(scorecard, findings);
|
|
222
|
+
}
|
|
223
|
+
scorecard.resumability = resumeObservation.complete ? "pass" : "warn";
|
|
224
|
+
if (!resumeObservation.complete) {
|
|
225
|
+
findings.push(finding("plugin.runtime.remote.reliability.resume.inconclusive", "warn", "The remote MCP SSE resume response ended before a complete event could be observed.", "Emit complete SSE events after accepting a reconnect."));
|
|
226
|
+
return finalize(scorecard, findings);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!options.allowSessionLifecycle || currentSessionId === null) {
|
|
232
|
+
return finalize(scorecard, findings);
|
|
233
|
+
}
|
|
234
|
+
const terminated = await send("DELETE");
|
|
235
|
+
if (terminated.restartFailed || terminated.error || !terminated.response
|
|
236
|
+
|| (terminated.response.statusCode !== 405 && (terminated.response.statusCode < 200 || terminated.response.statusCode >= 300))) {
|
|
237
|
+
scorecard.termination = "fail";
|
|
238
|
+
findings.push(finding("plugin.runtime.remote.reliability.termination.failed", "fail", "The remote MCP session could not be terminated with the approved lifecycle request.", "Return a successful response or HTTP 405 for a bounded MCP session DELETE request."));
|
|
239
|
+
return finalize(scorecard, findings);
|
|
240
|
+
}
|
|
241
|
+
scorecard.termination = "pass";
|
|
242
|
+
return finalize(scorecard, findings);
|
|
243
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DiscoveredPackage, RuntimeProbeResult, RuntimeSandboxMode } from "../domain/types.js";
|
|
1
|
+
import type { DiscoveredPackage, RuntimeProbeResult, RuntimeSandboxMode, RuntimeScorecard } from "../domain/types.js";
|
|
2
2
|
import { type RemoteMcpRequest } from "./remote-mcp-probe.js";
|
|
3
3
|
import type { RemoteLookup } from "./remote-network-policy.js";
|
|
4
4
|
export declare function probeRuntime(discoveredPackage: DiscoveredPackage, options?: RuntimeProbeOptions): Promise<RuntimeProbeResult>;
|
|
@@ -8,8 +8,10 @@ export interface RuntimeProbeOptions {
|
|
|
8
8
|
transcript?: (line: string) => void;
|
|
9
9
|
allowNetwork?: boolean;
|
|
10
10
|
allowLocalNetwork?: boolean;
|
|
11
|
+
allowSessionLifecycle?: boolean;
|
|
11
12
|
remoteRequestTimeoutMs?: number;
|
|
12
13
|
remoteLookup?: RemoteLookup;
|
|
13
14
|
remoteRequest?: RemoteMcpRequest;
|
|
14
15
|
}
|
|
16
|
+
export declare function remoteReliabilityGatePassed(scorecard: RuntimeScorecard | undefined): boolean;
|
|
15
17
|
export declare function probeRuntimeConfig(rootPath: string, mcpConfigPath: string, options?: RuntimeProbeOptions): Promise<RuntimeProbeResult>;
|
|
@@ -345,6 +345,17 @@ function mergeRemoteRuntimeScorecards(left, right) {
|
|
|
345
345
|
"present-valid": 1,
|
|
346
346
|
absent: 0
|
|
347
347
|
};
|
|
348
|
+
const reliability = left.reliability && right.reliability
|
|
349
|
+
? {
|
|
350
|
+
getSse: worstRuntimeStatus(left.reliability.getSse, right.reliability.getSse),
|
|
351
|
+
sessionPropagation: worstRuntimeStatus(left.reliability.sessionPropagation, right.reliability.sessionPropagation),
|
|
352
|
+
resumability: worstRuntimeStatus(left.reliability.resumability, right.reliability.resumability),
|
|
353
|
+
disconnectSafety: worstRuntimeStatus(left.reliability.disconnectSafety, right.reliability.disconnectSafety),
|
|
354
|
+
sessionRestart: worstRuntimeStatus(left.reliability.sessionRestart, right.reliability.sessionRestart),
|
|
355
|
+
termination: worstRuntimeStatus(left.reliability.termination, right.reliability.termination),
|
|
356
|
+
overall: worstRuntimeStatus(left.reliability.overall, right.reliability.overall)
|
|
357
|
+
}
|
|
358
|
+
: left.reliability ?? right.reliability;
|
|
348
359
|
return {
|
|
349
360
|
transport: worstRuntimeStatus(left.transport, right.transport),
|
|
350
361
|
networkSafety: worstRuntimeStatus(left.networkSafety, right.networkSafety),
|
|
@@ -355,7 +366,8 @@ function mergeRemoteRuntimeScorecards(left, right) {
|
|
|
355
366
|
: right.session,
|
|
356
367
|
protocolHeaders: worstRuntimeStatus(left.protocolHeaders, right.protocolHeaders),
|
|
357
368
|
authorization: worstRuntimeStatus(left.authorization, right.authorization),
|
|
358
|
-
overall: worstRuntimeStatus(left.overall, right.overall)
|
|
369
|
+
overall: worstRuntimeStatus(left.overall, right.overall),
|
|
370
|
+
...(reliability ? { reliability } : {})
|
|
359
371
|
};
|
|
360
372
|
}
|
|
361
373
|
function supportsTasksListProbe(protocolVersion, capabilities) {
|
|
@@ -1251,6 +1263,10 @@ export async function probeRuntime(discoveredPackage, options = {}) {
|
|
|
1251
1263
|
}
|
|
1252
1264
|
return probeRuntimeConfig(rootPath, manifest.mcpServers, options);
|
|
1253
1265
|
}
|
|
1266
|
+
export function remoteReliabilityGatePassed(scorecard) {
|
|
1267
|
+
return scorecard !== undefined &&
|
|
1268
|
+
(scorecard.remote === undefined || scorecard.remote.reliability?.overall === "pass");
|
|
1269
|
+
}
|
|
1254
1270
|
export async function probeRuntimeConfig(rootPath, mcpConfigPath, options = {}) {
|
|
1255
1271
|
const resolvedRootPath = path.resolve(rootPath);
|
|
1256
1272
|
const resolvedMcpConfigPath = path.resolve(resolvedRootPath, mcpConfigPath);
|
|
@@ -1296,6 +1312,7 @@ export async function probeRuntimeConfig(rootPath, mcpConfigPath, options = {})
|
|
|
1296
1312
|
const remote = await probeRemoteMcpServer(serverName, url, {
|
|
1297
1313
|
allowNetwork: options.allowNetwork,
|
|
1298
1314
|
allowLocalNetwork: options.allowLocalNetwork,
|
|
1315
|
+
allowSessionLifecycle: options.allowSessionLifecycle,
|
|
1299
1316
|
requestTimeoutMs: options.remoteRequestTimeoutMs,
|
|
1300
1317
|
lookup: options.remoteLookup,
|
|
1301
1318
|
request: options.remoteRequest
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const MAX_SSE_FIELD_BYTES = 1_024;
|
|
2
|
+
function isSafeEventId(value) {
|
|
3
|
+
return value.length > 0
|
|
4
|
+
&& value.length <= MAX_SSE_FIELD_BYTES
|
|
5
|
+
&& value.every((byte) => byte >= 0x21 && byte <= 0x7e);
|
|
6
|
+
}
|
|
7
|
+
function parseRetry(value) {
|
|
8
|
+
if (value.length === 0 || value.length > MAX_SSE_FIELD_BYTES) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
for (const byte of value) {
|
|
12
|
+
if (byte < 0x30 || byte > 0x39) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const retryMs = Number(value.toString("ascii"));
|
|
17
|
+
return Number.isSafeInteger(retryMs) ? retryMs : null;
|
|
18
|
+
}
|
|
19
|
+
export function observeFirstSseEvent(body) {
|
|
20
|
+
let eventId = null;
|
|
21
|
+
let retryMs = null;
|
|
22
|
+
let malformed = false;
|
|
23
|
+
let lineStart = 0;
|
|
24
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
25
|
+
if (body[index] !== 0x0a && body[index] !== 0x0d) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const line = body.subarray(lineStart, index);
|
|
29
|
+
if (body[index] === 0x0d && body[index + 1] === 0x0a) {
|
|
30
|
+
index += 1;
|
|
31
|
+
}
|
|
32
|
+
lineStart = index + 1;
|
|
33
|
+
if (line.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
complete: true,
|
|
36
|
+
eventId: malformed ? null : eventId,
|
|
37
|
+
retryMs: malformed ? null : retryMs,
|
|
38
|
+
malformed
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (line[0] === 0x3a) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const separator = line.indexOf(0x3a);
|
|
45
|
+
const field = separator === -1 ? line : line.subarray(0, separator);
|
|
46
|
+
const valueStart = separator === -1 ? line.length : separator + 1;
|
|
47
|
+
const value = line.subarray(valueStart + (line[valueStart] === 0x20 ? 1 : 0));
|
|
48
|
+
if (field.equals(Buffer.from("id"))) {
|
|
49
|
+
if (value.length === 0) {
|
|
50
|
+
eventId = null;
|
|
51
|
+
}
|
|
52
|
+
else if (isSafeEventId(value)) {
|
|
53
|
+
eventId = value.toString("ascii");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
malformed = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (field.equals(Buffer.from("retry"))) {
|
|
60
|
+
const parsedRetryMs = parseRetry(value);
|
|
61
|
+
if (parsedRetryMs !== null) {
|
|
62
|
+
retryMs = parsedRetryMs;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { complete: false, eventId: null, retryMs: null, malformed: false };
|
|
67
|
+
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
4
4
|
import { discoverPackage } from "./discover-package.js";
|
|
5
5
|
import { inspectRemoteMcpUrl } from "./remote-url-policy.js";
|
|
6
|
-
import { probeRuntime } from "./runtime-probe.js";
|
|
6
|
+
import { probeRuntime, remoteReliabilityGatePassed } from "./runtime-probe.js";
|
|
7
7
|
function buildFailure(id, message, impact, suggestedFix, evidence) {
|
|
8
8
|
return {
|
|
9
9
|
id,
|
|
@@ -446,7 +446,8 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
446
446
|
sandbox: options.runtimeSandbox,
|
|
447
447
|
transcript: options.runtimeTranscript,
|
|
448
448
|
allowNetwork: options.allowNetwork,
|
|
449
|
-
allowLocalNetwork: options.allowLocalNetwork
|
|
449
|
+
allowLocalNetwork: options.allowLocalNetwork,
|
|
450
|
+
allowSessionLifecycle: options.allowSessionLifecycle
|
|
450
451
|
})
|
|
451
452
|
: null;
|
|
452
453
|
const findings = [
|
|
@@ -454,7 +455,7 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
454
455
|
...(runtimeResult?.findings ?? [])
|
|
455
456
|
];
|
|
456
457
|
const fingerprintedFindings = withFindingFingerprints(findings, discoveredPackage.rootPath);
|
|
457
|
-
const hasFailures = fingerprintedFindings.some((finding) => finding.severity === "fail");
|
|
458
|
+
const hasFailures = fingerprintedFindings.some((finding) => finding.severity === "fail") || (options.requireRemoteReliability === true && !remoteReliabilityGatePassed(runtimeResult?.scorecard));
|
|
458
459
|
const hasWarnings = fingerprintedFindings.some((finding) => finding.severity === "warn");
|
|
459
460
|
if (!hasFailures && !hasWarnings) {
|
|
460
461
|
return {
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface CheckOptions {
|
|
|
43
43
|
runtime?: boolean;
|
|
44
44
|
allowNetwork?: boolean;
|
|
45
45
|
allowLocalNetwork?: boolean;
|
|
46
|
+
allowSessionLifecycle?: boolean;
|
|
47
|
+
requireRemoteReliability?: boolean;
|
|
46
48
|
runtimeTranscript?: (line: string) => void;
|
|
47
49
|
runtimeStartupTimeoutMs?: number;
|
|
48
50
|
runtimeSandbox?: RuntimeSandboxMode;
|
|
@@ -103,8 +105,19 @@ export interface RemoteRuntimeScorecard {
|
|
|
103
105
|
session: "absent" | "present-valid" | "present-invalid";
|
|
104
106
|
protocolHeaders: RuntimeCapabilityStatus;
|
|
105
107
|
authorization: RuntimeCapabilityStatus;
|
|
108
|
+
reliability?: RemoteTransportReliabilityScorecard;
|
|
106
109
|
overall: "pass" | "warn" | "fail" | "skipped";
|
|
107
110
|
}
|
|
111
|
+
export type RemoteTransportReliabilityStatus = "pass" | "warn" | "fail" | "skipped";
|
|
112
|
+
export interface RemoteTransportReliabilityScorecard {
|
|
113
|
+
getSse: RemoteTransportReliabilityStatus;
|
|
114
|
+
sessionPropagation: RemoteTransportReliabilityStatus;
|
|
115
|
+
resumability: RemoteTransportReliabilityStatus;
|
|
116
|
+
disconnectSafety: RemoteTransportReliabilityStatus;
|
|
117
|
+
sessionRestart: RemoteTransportReliabilityStatus;
|
|
118
|
+
termination: RemoteTransportReliabilityStatus;
|
|
119
|
+
overall: RemoteTransportReliabilityStatus;
|
|
120
|
+
}
|
|
108
121
|
export type McpConformanceProfile = "legacy" | "2025-11-25" | "future-compatible";
|
|
109
122
|
export interface RuntimeConformanceScorecard {
|
|
110
123
|
protocolVersion: string | null;
|
|
@@ -17,6 +17,8 @@ export interface GenericMcpDoctorOptions {
|
|
|
17
17
|
runtime?: boolean;
|
|
18
18
|
allowNetwork?: boolean;
|
|
19
19
|
allowLocalNetwork?: boolean;
|
|
20
|
+
allowSessionLifecycle?: boolean;
|
|
21
|
+
requireRemoteReliability?: boolean;
|
|
20
22
|
runtimeStartupTimeoutMs?: number;
|
|
21
23
|
}
|
|
22
24
|
export declare function buildGenericMcpDoctor(targetPath: string, environment?: CompatibilityEnvironment, options?: GenericMcpDoctorOptions): Promise<GenericMcpDoctorReport>;
|
|
@@ -2,7 +2,7 @@ import { realpath, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildCompatibilityMatrix, readMcpConfigPath } from "../compatibility/compatibility-matrix.js";
|
|
4
4
|
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
|
-
import { probeRuntimeConfig } from "../core/runtime-probe.js";
|
|
5
|
+
import { probeRuntimeConfig, remoteReliabilityGatePassed } from "../core/runtime-probe.js";
|
|
6
6
|
import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
7
7
|
import { auditMcpServerConfig, buildSecurityAuditFromFindings } from "../security/security-audit.js";
|
|
8
8
|
function buildFinding(severity, id, message, impact, suggestedFix, evidence) {
|
|
@@ -142,11 +142,14 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
|
|
|
142
142
|
? await probeRuntimeConfig(canonicalRootPath, canonicalMcpConfigPath, {
|
|
143
143
|
startupTimeoutMs: options.runtimeStartupTimeoutMs,
|
|
144
144
|
allowNetwork: options.allowNetwork,
|
|
145
|
-
allowLocalNetwork: options.allowLocalNetwork
|
|
145
|
+
allowLocalNetwork: options.allowLocalNetwork,
|
|
146
|
+
allowSessionLifecycle: options.allowSessionLifecycle
|
|
146
147
|
})
|
|
147
148
|
: null;
|
|
148
149
|
const fingerprintedFindings = withFindingFingerprints([...staticFindings, ...(runtimeResult?.findings ?? [])], rootPath);
|
|
149
|
-
const status =
|
|
150
|
+
const status = options.requireRemoteReliability === true && !remoteReliabilityGatePassed(runtimeResult?.scorecard)
|
|
151
|
+
? "fail"
|
|
152
|
+
: mergeReportStatus(fingerprintedFindings, security);
|
|
150
153
|
return {
|
|
151
154
|
targetPath: rootPath,
|
|
152
155
|
status,
|
|
@@ -42,6 +42,19 @@ function appendRuntimeScorecard(lines, result) {
|
|
|
42
42
|
lines.push(`| Protocol headers | ${remote.protocolHeaders.toUpperCase()} |`);
|
|
43
43
|
lines.push(`| Authorization | ${remote.authorization.toUpperCase()} |`);
|
|
44
44
|
lines.push(`| Overall | ${remote.overall.toUpperCase()} |`);
|
|
45
|
+
if (remote.reliability) {
|
|
46
|
+
const reliability = remote.reliability;
|
|
47
|
+
lines.push("", "## Remote Transport Reliability", "");
|
|
48
|
+
lines.push("| Check | Status |");
|
|
49
|
+
lines.push("| --- | --- |");
|
|
50
|
+
lines.push(`| GET SSE | ${reliability.getSse.toUpperCase()} |`);
|
|
51
|
+
lines.push(`| Session propagation | ${reliability.sessionPropagation.toUpperCase()} |`);
|
|
52
|
+
lines.push(`| Resumability | ${reliability.resumability.toUpperCase()} |`);
|
|
53
|
+
lines.push(`| Disconnect safety | ${reliability.disconnectSafety.toUpperCase()} |`);
|
|
54
|
+
lines.push(`| Session restart | ${reliability.sessionRestart.toUpperCase()} |`);
|
|
55
|
+
lines.push(`| Termination | ${reliability.termination.toUpperCase()} |`);
|
|
56
|
+
lines.push(`| Overall | ${reliability.overall.toUpperCase()} |`);
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
60
|
export function buildMarkdownReport(result, options) {
|
|
@@ -59,6 +59,17 @@ function appendRuntimeScorecard(lines, result) {
|
|
|
59
59
|
lines.push(`protocol headers: ${remote.protocolHeaders}`);
|
|
60
60
|
lines.push(`authorization: ${remote.authorization}`);
|
|
61
61
|
lines.push(`overall: ${remote.overall}`);
|
|
62
|
+
if (remote.reliability) {
|
|
63
|
+
const reliability = remote.reliability;
|
|
64
|
+
lines.push("", "Remote Transport Reliability", "----------------------------");
|
|
65
|
+
lines.push(`GET SSE: ${reliability.getSse}`);
|
|
66
|
+
lines.push(`Session propagation: ${reliability.sessionPropagation}`);
|
|
67
|
+
lines.push(`Resumability: ${reliability.resumability}`);
|
|
68
|
+
lines.push(`Disconnect safety: ${reliability.disconnectSafety}`);
|
|
69
|
+
lines.push(`Session restart: ${reliability.sessionRestart}`);
|
|
70
|
+
lines.push(`Termination: ${reliability.termination}`);
|
|
71
|
+
lines.push(`Overall: ${reliability.overall}`);
|
|
72
|
+
}
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
export function renderTextReport(result, options = {}) {
|
|
@@ -556,6 +556,114 @@ export const ruleCatalog = [
|
|
|
556
556
|
why: "Authorization readiness cannot be confirmed without bounded metadata responses.",
|
|
557
557
|
fix: "Make protected-resource and authorization-server metadata available over HTTPS.",
|
|
558
558
|
example: "Return application/json discovery metadata."
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: "plugin.runtime.remote.reliability.get.status",
|
|
562
|
+
category: "runtime",
|
|
563
|
+
defaultSeverity: "fail",
|
|
564
|
+
summary: "The remote MCP endpoint rejected the SSE transport request.",
|
|
565
|
+
why: "Clients cannot rely on server-to-client streaming when an accepted SSE request does not return the expected status.",
|
|
566
|
+
fix: "Return HTTP 200 with a valid SSE response, or HTTP 405 when GET streaming is not supported.",
|
|
567
|
+
example: "HTTP/1.1 405 Method Not Allowed"
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
id: "plugin.runtime.remote.reliability.get.content_type",
|
|
571
|
+
category: "runtime",
|
|
572
|
+
defaultSeverity: "fail",
|
|
573
|
+
summary: "The remote MCP endpoint returned a non-SSE media type for GET streaming.",
|
|
574
|
+
why: "Clients cannot interpret a successful streaming response unless it is declared as Server-Sent Events.",
|
|
575
|
+
fix: "Return Content-Type: text/event-stream for accepted GET streaming requests.",
|
|
576
|
+
example: "Content-Type: text/event-stream"
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
id: "plugin.runtime.remote.reliability.get.inconclusive",
|
|
580
|
+
category: "runtime",
|
|
581
|
+
defaultSeverity: "warn",
|
|
582
|
+
summary: "The remote MCP GET streaming check was inconclusive.",
|
|
583
|
+
why: "A bounded probe could not observe a complete SSE event, so streaming reliability remains uncertain.",
|
|
584
|
+
fix: "Emit complete SSE event frames promptly when server-to-client streaming is supported.",
|
|
585
|
+
example: "id: event-1\n\ndata: {}\n\n"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
id: "plugin.runtime.remote.reliability.get.failed",
|
|
589
|
+
category: "runtime",
|
|
590
|
+
defaultSeverity: "fail",
|
|
591
|
+
summary: "The remote MCP GET streaming request could not be completed.",
|
|
592
|
+
why: "Clients cannot rely on server-to-client streaming when a bounded transport request fails.",
|
|
593
|
+
fix: "Keep the Streamable HTTP transport reachable within the configured request bounds.",
|
|
594
|
+
example: "Complete the GET request before the runtime timeout."
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
id: "plugin.runtime.remote.reliability.get.malformed",
|
|
598
|
+
category: "runtime",
|
|
599
|
+
defaultSeverity: "fail",
|
|
600
|
+
summary: "The remote MCP endpoint emitted malformed SSE framing.",
|
|
601
|
+
why: "Malformed event framing prevents clients from safely processing stream progress or reconnecting.",
|
|
602
|
+
fix: "Return complete SSE events with valid id and retry fields.",
|
|
603
|
+
example: "id: event-1\n\nevent: message\n\ndata: {}\n\n"
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
id: "plugin.runtime.remote.reliability.resume.status",
|
|
607
|
+
category: "runtime",
|
|
608
|
+
defaultSeverity: "fail",
|
|
609
|
+
summary: "The remote MCP endpoint rejected the SSE resume request.",
|
|
610
|
+
why: "Clients cannot resume a dropped stream when the reconnect response has an unexpected status.",
|
|
611
|
+
fix: "Return HTTP 200 for an accepted Last-Event-ID reconnect request.",
|
|
612
|
+
example: "HTTP/1.1 200 OK"
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: "plugin.runtime.remote.reliability.resume.content_type",
|
|
616
|
+
category: "runtime",
|
|
617
|
+
defaultSeverity: "fail",
|
|
618
|
+
summary: "The remote MCP endpoint returned a non-SSE media type for a resume request.",
|
|
619
|
+
why: "Clients cannot process a resumed stream unless it is declared as Server-Sent Events.",
|
|
620
|
+
fix: "Return Content-Type: text/event-stream after accepting a Last-Event-ID reconnect request.",
|
|
621
|
+
example: "Content-Type: text/event-stream"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "plugin.runtime.remote.reliability.resume.inconclusive",
|
|
625
|
+
category: "runtime",
|
|
626
|
+
defaultSeverity: "warn",
|
|
627
|
+
summary: "The remote MCP SSE resume check was inconclusive.",
|
|
628
|
+
why: "A bounded probe could not confirm a complete resumed event, so reconnect reliability remains uncertain.",
|
|
629
|
+
fix: "Emit complete SSE event frames after accepting a reconnect.",
|
|
630
|
+
example: "Return one complete SSE event after a Last-Event-ID request."
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: "plugin.runtime.remote.reliability.resume.failed",
|
|
634
|
+
category: "runtime",
|
|
635
|
+
defaultSeverity: "fail",
|
|
636
|
+
summary: "The remote MCP SSE resume request could not be completed.",
|
|
637
|
+
why: "Clients cannot recover from a dropped stream when a bounded reconnect fails.",
|
|
638
|
+
fix: "Accept one bounded SSE reconnect using Last-Event-ID.",
|
|
639
|
+
example: "Honor the Last-Event-ID header on one reconnect."
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "plugin.runtime.remote.reliability.resume.malformed",
|
|
643
|
+
category: "runtime",
|
|
644
|
+
defaultSeverity: "fail",
|
|
645
|
+
summary: "The remote MCP endpoint emitted malformed SSE framing after reconnecting.",
|
|
646
|
+
why: "Malformed resumed events prevent clients from safely continuing stream processing.",
|
|
647
|
+
fix: "Return complete SSE events with valid id and retry fields after reconnecting.",
|
|
648
|
+
example: "Return a complete event frame after reconnecting."
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: "plugin.runtime.remote.reliability.session_restart.failed",
|
|
652
|
+
category: "runtime",
|
|
653
|
+
defaultSeverity: "fail",
|
|
654
|
+
summary: "The remote MCP session could not be restarted.",
|
|
655
|
+
why: "Clients cannot recover from an expired MCP session when a fresh initialization sequence fails.",
|
|
656
|
+
fix: "Accept a fresh initialize sequence after an expired MCP session.",
|
|
657
|
+
example: "Return a valid initialize response after HTTP 404 for a session-bound request."
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: "plugin.runtime.remote.reliability.termination.failed",
|
|
661
|
+
category: "runtime",
|
|
662
|
+
defaultSeverity: "fail",
|
|
663
|
+
summary: "The remote MCP session could not be terminated.",
|
|
664
|
+
why: "A server that advertises session lifecycle support must safely handle the approved termination request.",
|
|
665
|
+
fix: "Return a successful response or HTTP 405 for a bounded MCP session DELETE request.",
|
|
666
|
+
example: "HTTP/1.1 204 No Content"
|
|
559
667
|
}
|
|
560
668
|
];
|
|
561
669
|
export function findRuleDefinition(id) {
|
package/dist/run-cli.js
CHANGED
|
@@ -102,6 +102,8 @@ function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
|
|
|
102
102
|
function parseRemoteNetworkFlags(flags, runtime) {
|
|
103
103
|
let allowNetwork = false;
|
|
104
104
|
let allowLocalNetwork = false;
|
|
105
|
+
let allowSessionLifecycle = false;
|
|
106
|
+
let requireRemoteReliability = false;
|
|
105
107
|
for (let index = 0; index < flags.length; index += 1) {
|
|
106
108
|
const flag = flags[index];
|
|
107
109
|
if (flag === "--allow-network") {
|
|
@@ -120,23 +122,55 @@ function parseRemoteNetworkFlags(flags, runtime) {
|
|
|
120
122
|
}
|
|
121
123
|
allowLocalNetwork = true;
|
|
122
124
|
}
|
|
123
|
-
else if (flag
|
|
125
|
+
else if (flag === "--allow-session-lifecycle") {
|
|
126
|
+
if (allowSessionLifecycle)
|
|
127
|
+
return new CliUsageError("Duplicate runtime remote flag: --allow-session-lifecycle.");
|
|
128
|
+
if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
|
|
129
|
+
return new CliUsageError("--allow-session-lifecycle does not accept a value.");
|
|
130
|
+
}
|
|
131
|
+
allowSessionLifecycle = true;
|
|
132
|
+
}
|
|
133
|
+
else if (flag === "--require-remote-reliability") {
|
|
134
|
+
if (requireRemoteReliability)
|
|
135
|
+
return new CliUsageError("Duplicate runtime remote flag: --require-remote-reliability.");
|
|
136
|
+
if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
|
|
137
|
+
return new CliUsageError("--require-remote-reliability does not accept a value.");
|
|
138
|
+
}
|
|
139
|
+
requireRemoteReliability = true;
|
|
140
|
+
}
|
|
141
|
+
else if (flag?.startsWith("--allow-network=") ||
|
|
142
|
+
flag?.startsWith("--allow-local-network=") ||
|
|
143
|
+
flag?.startsWith("--allow-session-lifecycle=") ||
|
|
144
|
+
flag?.startsWith("--require-remote-reliability=")) {
|
|
124
145
|
return new CliUsageError(`${flag.split("=", 1)[0]} does not accept a value.`);
|
|
125
146
|
}
|
|
126
147
|
}
|
|
148
|
+
if (allowSessionLifecycle && !runtime) {
|
|
149
|
+
return new CliUsageError("--allow-session-lifecycle requires --runtime.");
|
|
150
|
+
}
|
|
151
|
+
if (requireRemoteReliability && !runtime) {
|
|
152
|
+
return new CliUsageError("--require-remote-reliability requires --runtime and --allow-network.");
|
|
153
|
+
}
|
|
127
154
|
if ((allowNetwork || allowLocalNetwork) && !runtime) {
|
|
128
155
|
return new CliUsageError("--allow-network requires --runtime.");
|
|
129
156
|
}
|
|
130
157
|
if (allowLocalNetwork && !allowNetwork) {
|
|
131
158
|
return new CliUsageError("--allow-local-network requires --allow-network.");
|
|
132
159
|
}
|
|
133
|
-
|
|
160
|
+
if (allowSessionLifecycle && !allowNetwork) {
|
|
161
|
+
return new CliUsageError("--allow-session-lifecycle requires --allow-network.");
|
|
162
|
+
}
|
|
163
|
+
if (requireRemoteReliability && !allowNetwork) {
|
|
164
|
+
return new CliUsageError("--require-remote-reliability requires --runtime and --allow-network.");
|
|
165
|
+
}
|
|
166
|
+
return { allowNetwork, allowLocalNetwork, allowSessionLifecycle, requireRemoteReliability };
|
|
134
167
|
}
|
|
135
168
|
function printUsage(io) {
|
|
136
169
|
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");
|
|
137
170
|
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>]");
|
|
138
171
|
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]");
|
|
139
172
|
io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
|
|
173
|
+
io.writeStderr("Remote MCP runtime flags (check, mcp, release check, and doctor release-evidence): --runtime --allow-network [--allow-local-network] [--allow-session-lifecycle] [--require-remote-reliability]\n --allow-session-lifecycle requires --runtime --allow-network and can terminate a remote session.\n --require-remote-reliability requires --runtime --allow-network, blocks non-pass reliability, and does not grant network consent.");
|
|
140
174
|
}
|
|
141
175
|
const suppressUsageText = [
|
|
142
176
|
"Usage:",
|
|
@@ -687,6 +721,8 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
|
|
|
687
721
|
let runtime = false;
|
|
688
722
|
let allowNetwork = false;
|
|
689
723
|
let allowLocalNetwork = false;
|
|
724
|
+
let allowSessionLifecycle = false;
|
|
725
|
+
let requireRemoteReliability = false;
|
|
690
726
|
for (let index = 0; index < flags.length; index += 1) {
|
|
691
727
|
const flag = flags[index];
|
|
692
728
|
if (flag === "--runtime") {
|
|
@@ -715,6 +751,26 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
|
|
|
715
751
|
allowLocalNetwork = true;
|
|
716
752
|
continue;
|
|
717
753
|
}
|
|
754
|
+
if (flag === "--allow-session-lifecycle") {
|
|
755
|
+
if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
|
|
756
|
+
return "--allow-session-lifecycle does not accept a value.";
|
|
757
|
+
}
|
|
758
|
+
allowSessionLifecycle = true;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (flag === "--require-remote-reliability") {
|
|
762
|
+
if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
|
|
763
|
+
return "--require-remote-reliability does not accept a value.";
|
|
764
|
+
}
|
|
765
|
+
requireRemoteReliability = true;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (flag.startsWith("--allow-network=") ||
|
|
769
|
+
flag.startsWith("--allow-local-network=") ||
|
|
770
|
+
flag.startsWith("--allow-session-lifecycle=") ||
|
|
771
|
+
flag.startsWith("--require-remote-reliability=")) {
|
|
772
|
+
return `${flag.split("=", 1)[0]} does not accept a value.`;
|
|
773
|
+
}
|
|
718
774
|
if (flag === "--output") {
|
|
719
775
|
if (outputPath !== null) {
|
|
720
776
|
return "Duplicate MCP flag: --output.";
|
|
@@ -740,8 +796,10 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
|
|
|
740
796
|
jsonOutput,
|
|
741
797
|
outputPath,
|
|
742
798
|
runtime,
|
|
743
|
-
allowNetwork,
|
|
744
|
-
allowLocalNetwork
|
|
799
|
+
allowNetwork: remoteNetwork.allowNetwork,
|
|
800
|
+
allowLocalNetwork: remoteNetwork.allowLocalNetwork,
|
|
801
|
+
allowSessionLifecycle,
|
|
802
|
+
requireRemoteReliability
|
|
745
803
|
};
|
|
746
804
|
}
|
|
747
805
|
function parsePerformanceThresholds(flags) {
|
|
@@ -1031,7 +1089,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1031
1089
|
}, {
|
|
1032
1090
|
runtime: parsedMcpArgs.runtime,
|
|
1033
1091
|
allowNetwork: parsedMcpArgs.allowNetwork,
|
|
1034
|
-
allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
|
|
1092
|
+
allowLocalNetwork: parsedMcpArgs.allowLocalNetwork,
|
|
1093
|
+
allowSessionLifecycle: parsedMcpArgs.allowSessionLifecycle,
|
|
1094
|
+
requireRemoteReliability: parsedMcpArgs.requireRemoteReliability
|
|
1035
1095
|
});
|
|
1036
1096
|
const renderedReport = parsedMcpArgs.jsonOutput
|
|
1037
1097
|
? renderGenericMcpDoctorJson(report)
|
|
@@ -1344,6 +1404,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1344
1404
|
runtime,
|
|
1345
1405
|
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
1346
1406
|
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
1407
|
+
...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
1408
|
+
...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
1347
1409
|
...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
|
|
1348
1410
|
environment: {
|
|
1349
1411
|
env: terminalContext.env,
|
|
@@ -1507,6 +1569,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1507
1569
|
runtime,
|
|
1508
1570
|
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
1509
1571
|
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
1572
|
+
...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
1573
|
+
...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
1510
1574
|
...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
|
|
1511
1575
|
environment: {
|
|
1512
1576
|
env: terminalContext.env,
|
|
@@ -2332,7 +2396,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2332
2396
|
}, {
|
|
2333
2397
|
runtime: parsedMcpArgs.runtime,
|
|
2334
2398
|
allowNetwork: parsedMcpArgs.allowNetwork,
|
|
2335
|
-
allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
|
|
2399
|
+
allowLocalNetwork: parsedMcpArgs.allowLocalNetwork,
|
|
2400
|
+
allowSessionLifecycle: parsedMcpArgs.allowSessionLifecycle,
|
|
2401
|
+
requireRemoteReliability: parsedMcpArgs.requireRemoteReliability
|
|
2336
2402
|
});
|
|
2337
2403
|
const renderedReport = parsedMcpArgs.jsonOutput
|
|
2338
2404
|
? renderGenericMcpDoctorJson(report)
|
|
@@ -2653,6 +2719,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2653
2719
|
runtime: runtimeProbeEnabled,
|
|
2654
2720
|
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2655
2721
|
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
2722
|
+
...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
2723
|
+
...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
2656
2724
|
...(runtimeSandbox ? { runtimeSandbox } : {}),
|
|
2657
2725
|
runCheck: options.runCheckImpl
|
|
2658
2726
|
});
|
|
@@ -2867,6 +2935,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2867
2935
|
runtime: effectiveRuntimeProbeEnabled,
|
|
2868
2936
|
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2869
2937
|
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
2938
|
+
...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
2939
|
+
...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
2870
2940
|
runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
|
|
2871
2941
|
? (line) => io.writeStderr(line)
|
|
2872
2942
|
: undefined
|
|
@@ -2938,7 +3008,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2938
3008
|
const pluginResult = applyDoctorConfig(await runCheckImpl(pluginRoot, {
|
|
2939
3009
|
runtime: effectiveRuntimeProbeEnabled,
|
|
2940
3010
|
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2941
|
-
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {})
|
|
3011
|
+
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
3012
|
+
...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
3013
|
+
...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {})
|
|
2942
3014
|
}), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(pluginRoot, configPath), checkProfile), policy));
|
|
2943
3015
|
io.writeStdout(`\n${pluginResult.targetPath}: ${pluginResult.status.toUpperCase()}`);
|
|
2944
3016
|
validatedCount += 1;
|
|
@@ -2962,6 +3034,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2962
3034
|
runtime: effectiveRuntimeProbeEnabled,
|
|
2963
3035
|
...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
|
|
2964
3036
|
...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
|
|
3037
|
+
...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
|
|
3038
|
+
...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
|
|
2965
3039
|
...(runtimeSandbox ? { runtimeSandbox } : {}),
|
|
2966
3040
|
...(effectiveRuntimeProbeEnabled && verboseRuntime
|
|
2967
3041
|
? { runtimeTranscript: (line) => io.writeStderr(line) }
|
package/package.json
CHANGED