codex-plugin-doctor 1.51.0 → 1.52.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 +17 -6
- package/dist/core/bounded-http-client.d.ts +23 -0
- package/dist/core/bounded-http-client.js +240 -0
- package/dist/core/output-contract.js +23 -1
- 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 +111 -7
- package/dist/core/remote-mcp-probe.d.ts +16 -0
- package/dist/core/remote-mcp-probe.js +226 -0
- package/dist/core/remote-network-policy.d.ts +24 -0
- package/dist/core/remote-network-policy.js +125 -0
- package/dist/core/remote-oauth-readiness.d.ts +12 -0
- package/dist/core/remote-oauth-readiness.js +295 -0
- package/dist/core/remote-url-policy.d.ts +7 -0
- package/dist/core/remote-url-policy.js +49 -0
- package/dist/core/runtime-plan.d.ts +3 -0
- package/dist/core/runtime-plan.js +34 -2
- package/dist/core/runtime-policy.js +5 -1
- package/dist/core/runtime-probe.d.ts +7 -0
- package/dist/core/runtime-probe.js +72 -29
- package/dist/core/validate-plugin.js +32 -1
- package/dist/domain/types.d.ts +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +2 -0
- package/dist/mcp/generic-mcp-doctor.js +10 -3
- package/dist/reporting/render-markdown-report.js +25 -12
- package/dist/reporting/render-text-report.js +21 -10
- package/dist/rules/rule-catalog.js +181 -1
- package/dist/run-cli.js +98 -5
- package/dist/security/security-audit.js +34 -7
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface RemoteUrlInspection {
|
|
2
|
+
parsedUrl: URL | null;
|
|
3
|
+
sanitizedUrl: string | null;
|
|
4
|
+
isLoopbackHost: boolean;
|
|
5
|
+
issues: Array<"invalid" | "unsupported_scheme" | "credentials" | "query" | "fragment" | "ip_literal" | "insecure_non_loopback">;
|
|
6
|
+
}
|
|
7
|
+
export declare function inspectRemoteMcpUrl(rawUrl: string): RemoteUrlInspection;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function isIpLiteral(hostname) {
|
|
2
|
+
return (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname) ||
|
|
3
|
+
/^\[[0-9a-f:.]+\]$/i.test(hostname));
|
|
4
|
+
}
|
|
5
|
+
export function inspectRemoteMcpUrl(rawUrl) {
|
|
6
|
+
let parsedUrl;
|
|
7
|
+
try {
|
|
8
|
+
parsedUrl = new URL(rawUrl);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {
|
|
12
|
+
parsedUrl: null,
|
|
13
|
+
sanitizedUrl: null,
|
|
14
|
+
isLoopbackHost: false,
|
|
15
|
+
issues: ["invalid"]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const issues = [];
|
|
19
|
+
const isSupportedScheme = parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
|
20
|
+
const isAbsoluteHttpUrl = /^https?:\/\//i.test(rawUrl);
|
|
21
|
+
const isLoopbackHost = parsedUrl.hostname.toLowerCase() === "localhost";
|
|
22
|
+
if (!isSupportedScheme) {
|
|
23
|
+
issues.push("unsupported_scheme");
|
|
24
|
+
}
|
|
25
|
+
else if (!isAbsoluteHttpUrl || !parsedUrl.hostname) {
|
|
26
|
+
issues.push("invalid");
|
|
27
|
+
}
|
|
28
|
+
if (parsedUrl.username || parsedUrl.password) {
|
|
29
|
+
issues.push("credentials");
|
|
30
|
+
}
|
|
31
|
+
if (parsedUrl.search || rawUrl.includes("?")) {
|
|
32
|
+
issues.push("query");
|
|
33
|
+
}
|
|
34
|
+
if (parsedUrl.hash || rawUrl.includes("#")) {
|
|
35
|
+
issues.push("fragment");
|
|
36
|
+
}
|
|
37
|
+
if (isIpLiteral(parsedUrl.hostname)) {
|
|
38
|
+
issues.push("ip_literal");
|
|
39
|
+
}
|
|
40
|
+
if (parsedUrl.protocol === "http:" && !isLoopbackHost) {
|
|
41
|
+
issues.push("insecure_non_loopback");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
parsedUrl,
|
|
45
|
+
sanitizedUrl: `${parsedUrl.origin}${parsedUrl.pathname}`,
|
|
46
|
+
isLoopbackHost,
|
|
47
|
+
issues
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { RuntimeExecutionEvidence, RuntimeSandboxMode } from "../domain/typ
|
|
|
4
4
|
type RuntimePlanStatus = "pass" | "warn" | "fail";
|
|
5
5
|
type RuntimePlanRiskLevel = "low" | "medium" | "high";
|
|
6
6
|
type RuntimePlanTransport = "stdio" | "http";
|
|
7
|
+
type RemoteNetworkClass = "public_https" | "loopback_http" | "loopback_https" | "invalid";
|
|
7
8
|
export interface RuntimePlanServer {
|
|
8
9
|
name: string;
|
|
9
10
|
transport: RuntimePlanTransport;
|
|
@@ -11,7 +12,9 @@ export interface RuntimePlanServer {
|
|
|
11
12
|
args: string[];
|
|
12
13
|
cwd: string | null;
|
|
13
14
|
url: string | null;
|
|
15
|
+
networkClass?: RemoteNetworkClass;
|
|
14
16
|
probeMethods: string[];
|
|
17
|
+
approvalRequirements?: string[];
|
|
15
18
|
riskLevel: RuntimePlanRiskLevel;
|
|
16
19
|
riskReasons: string[];
|
|
17
20
|
}
|
|
@@ -5,6 +5,7 @@ import { discoverPackage } from "./discover-package.js";
|
|
|
5
5
|
import { readJsonFile } from "./read-json-file.js";
|
|
6
6
|
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
7
7
|
import { DOCKER_RUNTIME_IMAGE } from "./runtime-sandbox.js";
|
|
8
|
+
import { inspectRemoteMcpUrl } from "./remote-url-policy.js";
|
|
8
9
|
function isPlainObject(value) {
|
|
9
10
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
11
|
}
|
|
@@ -47,6 +48,26 @@ function buildRisk(serverName, serverConfig, findings) {
|
|
|
47
48
|
riskReasons: [...new Set(riskReasons)]
|
|
48
49
|
};
|
|
49
50
|
}
|
|
51
|
+
function remoteProbeMethods() {
|
|
52
|
+
return [
|
|
53
|
+
"POST initialize",
|
|
54
|
+
"POST notifications/initialized",
|
|
55
|
+
"GET OAuth protected-resource metadata (401 only)",
|
|
56
|
+
"GET OAuth authorization-server metadata (401 only)"
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function remoteNetworkClass(rawUrl) {
|
|
60
|
+
const inspection = inspectRemoteMcpUrl(rawUrl);
|
|
61
|
+
if (inspection.parsedUrl === null ||
|
|
62
|
+
(inspection.parsedUrl.protocol !== "http:" && inspection.parsedUrl.protocol !== "https:") ||
|
|
63
|
+
!inspection.parsedUrl.hostname) {
|
|
64
|
+
return "invalid";
|
|
65
|
+
}
|
|
66
|
+
if (inspection.isLoopbackHost) {
|
|
67
|
+
return inspection.parsedUrl.protocol === "http:" ? "loopback_http" : "loopback_https";
|
|
68
|
+
}
|
|
69
|
+
return "public_https";
|
|
70
|
+
}
|
|
50
71
|
function planDigestPayload(plan) {
|
|
51
72
|
return {
|
|
52
73
|
schemaVersion: plan.schemaVersion,
|
|
@@ -119,6 +140,8 @@ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date(
|
|
|
119
140
|
const command = typeof serverConfig.command === "string" ? serverConfig.command : null;
|
|
120
141
|
const url = typeof serverConfig.url === "string" ? serverConfig.url : null;
|
|
121
142
|
const { riskLevel, riskReasons } = buildRisk(serverName, serverConfig, security.findings);
|
|
143
|
+
const networkClass = url ? remoteNetworkClass(url) : undefined;
|
|
144
|
+
const sanitizedUrl = url ? inspectRemoteMcpUrl(url).sanitizedUrl : null;
|
|
122
145
|
return {
|
|
123
146
|
name: serverName,
|
|
124
147
|
transport: command ? "stdio" : "http",
|
|
@@ -127,7 +150,8 @@ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date(
|
|
|
127
150
|
? serverConfig.args.filter((arg) => typeof arg === "string")
|
|
128
151
|
: [],
|
|
129
152
|
cwd: command ? normalizeCwd(discoveredPackage.rootPath, serverConfig.cwd) : null,
|
|
130
|
-
url,
|
|
153
|
+
url: sanitizedUrl,
|
|
154
|
+
...(networkClass ? { networkClass } : {}),
|
|
131
155
|
probeMethods: command
|
|
132
156
|
? [
|
|
133
157
|
"initialize",
|
|
@@ -140,7 +164,14 @@ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date(
|
|
|
140
164
|
"prompts/list",
|
|
141
165
|
"prompts/get:first-prompt-only"
|
|
142
166
|
]
|
|
143
|
-
: [],
|
|
167
|
+
: url ? remoteProbeMethods() : [],
|
|
168
|
+
...(url
|
|
169
|
+
? {
|
|
170
|
+
approvalRequirements: networkClass === "loopback_http" || networkClass === "loopback_https"
|
|
171
|
+
? ["--runtime", "--allow-network", "--allow-local-network"]
|
|
172
|
+
: ["--runtime", "--allow-network"]
|
|
173
|
+
}
|
|
174
|
+
: {}),
|
|
144
175
|
riskLevel,
|
|
145
176
|
riskReasons
|
|
146
177
|
};
|
|
@@ -247,6 +278,7 @@ export function renderDoctorRuntimePlanMarkdown(plan) {
|
|
|
247
278
|
"- This plan is non-executing.",
|
|
248
279
|
"- Probe methods explicitly exclude task create, get, result, and cancel operations, plus sampling and elicitation requests.",
|
|
249
280
|
"- Runtime probes require explicit operator approval before local MCP servers are started.",
|
|
281
|
+
"- Any remote target whose DNS resolves to loopback requires `--allow-local-network`, even if its URL looks public.",
|
|
250
282
|
"- The approval digest changes when command, args, cwd, probe methods, risk reasons, or findings change.",
|
|
251
283
|
"- Runtime approval is a review gate, not an OS, VM, or container sandbox.",
|
|
252
284
|
"",
|
|
@@ -38,7 +38,8 @@ function strongestDecision(decisions) {
|
|
|
38
38
|
return "allow";
|
|
39
39
|
}
|
|
40
40
|
function buildRecommendation(plan, decision, reasons) {
|
|
41
|
-
|
|
41
|
+
const hasRemoteServer = plan.servers.some((server) => server.transport === "http");
|
|
42
|
+
if (plan.summary.executableServerCount === 0 && !hasRemoteServer) {
|
|
42
43
|
return {
|
|
43
44
|
decision: "allow",
|
|
44
45
|
reason: "No executable local MCP runtime servers were found.",
|
|
@@ -78,6 +79,9 @@ function buildRecommendation(plan, decision, reasons) {
|
|
|
78
79
|
actions: [
|
|
79
80
|
"Review command, args, cwd, URL, probe methods, and risk reasons before execution.",
|
|
80
81
|
"Approve the exact plan digest with `check --runtime --require-runtime-approval --runtime-approval-digest <digest>`.",
|
|
82
|
+
...(hasRemoteServer
|
|
83
|
+
? ["Remote MCP probing also requires `--runtime --allow-network`; any target that DNS resolves to loopback additionally requires `--allow-local-network`."]
|
|
84
|
+
: []),
|
|
81
85
|
"Use `doctor runtime-plan --markdown` when the approval needs to be preserved with release evidence."
|
|
82
86
|
]
|
|
83
87
|
};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import type { DiscoveredPackage, RuntimeProbeResult, RuntimeSandboxMode } from "../domain/types.js";
|
|
2
|
+
import { type RemoteMcpRequest } from "./remote-mcp-probe.js";
|
|
3
|
+
import type { RemoteLookup } from "./remote-network-policy.js";
|
|
2
4
|
export declare function probeRuntime(discoveredPackage: DiscoveredPackage, options?: RuntimeProbeOptions): Promise<RuntimeProbeResult>;
|
|
3
5
|
export interface RuntimeProbeOptions {
|
|
4
6
|
startupTimeoutMs?: number;
|
|
5
7
|
sandbox?: RuntimeSandboxMode;
|
|
6
8
|
transcript?: (line: string) => void;
|
|
9
|
+
allowNetwork?: boolean;
|
|
10
|
+
allowLocalNetwork?: boolean;
|
|
11
|
+
remoteRequestTimeoutMs?: number;
|
|
12
|
+
remoteLookup?: RemoteLookup;
|
|
13
|
+
remoteRequest?: RemoteMcpRequest;
|
|
7
14
|
}
|
|
8
15
|
export declare function probeRuntimeConfig(rootPath: string, mcpConfigPath: string, options?: RuntimeProbeOptions): Promise<RuntimeProbeResult>;
|
|
@@ -4,6 +4,7 @@ import { readFile, realpath, stat } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { evaluateMcpConformance } from "./mcp-conformance.js";
|
|
7
|
+
import { probeRemoteMcpServer } from "./remote-mcp-probe.js";
|
|
7
8
|
import { buildRuntimeLaunch, DOCKER_RUNTIME_STARTUP_TIMEOUT_MS, RuntimeSandboxError } from "./runtime-sandbox.js";
|
|
8
9
|
import { formatRequestTranscript as formatRequestTranscriptForLog, formatResponseTranscript as formatResponseTranscriptForLog } from "./runtime-transcript.js";
|
|
9
10
|
const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
@@ -322,6 +323,9 @@ function mergeRuntimeScorecards(left, right) {
|
|
|
322
323
|
overall: worstRuntimeStatus(leftConformance.overall, rightConformance.overall)
|
|
323
324
|
}
|
|
324
325
|
: leftConformance ?? rightConformance;
|
|
326
|
+
const remote = left.remote && right.remote
|
|
327
|
+
? mergeRemoteRuntimeScorecards(left.remote, right.remote)
|
|
328
|
+
: left.remote ?? right.remote;
|
|
325
329
|
return {
|
|
326
330
|
initialize: worstRuntimeStatus(left.initialize, right.initialize),
|
|
327
331
|
toolsList: worstRuntimeStatus(left.toolsList, right.toolsList),
|
|
@@ -331,7 +335,27 @@ function mergeRuntimeScorecards(left, right) {
|
|
|
331
335
|
resourceTemplatesList: worstRuntimeStatus(left.resourceTemplatesList, right.resourceTemplatesList),
|
|
332
336
|
promptsList: worstRuntimeStatus(left.promptsList, right.promptsList),
|
|
333
337
|
promptGet: worstRuntimeStatus(left.promptGet, right.promptGet),
|
|
334
|
-
...(conformance ? { conformance } : {})
|
|
338
|
+
...(conformance ? { conformance } : {}),
|
|
339
|
+
...(remote ? { remote } : {})
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function mergeRemoteRuntimeScorecards(left, right) {
|
|
343
|
+
const sessionSeverity = {
|
|
344
|
+
"present-invalid": 2,
|
|
345
|
+
"present-valid": 1,
|
|
346
|
+
absent: 0
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
transport: worstRuntimeStatus(left.transport, right.transport),
|
|
350
|
+
networkSafety: worstRuntimeStatus(left.networkSafety, right.networkSafety),
|
|
351
|
+
initialize: worstRuntimeStatus(left.initialize, right.initialize),
|
|
352
|
+
contentType: worstRuntimeStatus(left.contentType, right.contentType),
|
|
353
|
+
session: sessionSeverity[left.session] >= sessionSeverity[right.session]
|
|
354
|
+
? left.session
|
|
355
|
+
: right.session,
|
|
356
|
+
protocolHeaders: worstRuntimeStatus(left.protocolHeaders, right.protocolHeaders),
|
|
357
|
+
authorization: worstRuntimeStatus(left.authorization, right.authorization),
|
|
358
|
+
overall: worstRuntimeStatus(left.overall, right.overall)
|
|
335
359
|
};
|
|
336
360
|
}
|
|
337
361
|
function supportsTasksListProbe(protocolVersion, capabilities) {
|
|
@@ -1266,35 +1290,54 @@ export async function probeRuntimeConfig(rootPath, mcpConfigPath, options = {})
|
|
|
1266
1290
|
if (!isPlainObject(config)) {
|
|
1267
1291
|
continue;
|
|
1268
1292
|
}
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
? (() => {
|
|
1279
|
-
const invalidCwdScorecard = createRuntimeScorecard();
|
|
1280
|
-
invalidCwdScorecard.initialize = "fail";
|
|
1281
|
-
return {
|
|
1282
|
-
findings: [
|
|
1283
|
-
withRuntimeEvidence(buildFailure("plugin.runtime.startup.invalid_cwd", `The MCP server \`${serverName}\` has an invalid runtime working directory.`, "Runtime validation must not start a server from a missing, non-directory, or out-of-package working directory.", "Set the MCP server cwd to an existing directory inside the plugin package root, or remove it."), serverName)
|
|
1284
|
-
],
|
|
1285
|
-
scorecard: invalidCwdScorecard
|
|
1286
|
-
};
|
|
1287
|
-
})()
|
|
1288
|
-
: await probeCommandServer({
|
|
1289
|
-
serverName,
|
|
1290
|
-
packageRoot: canonicalRootPath,
|
|
1291
|
-
command,
|
|
1292
|
-
args,
|
|
1293
|
-
cwd,
|
|
1294
|
-
startupTimeoutMs,
|
|
1295
|
-
sandbox: options.sandbox,
|
|
1296
|
-
transcript: options.transcript
|
|
1293
|
+
const url = config.url;
|
|
1294
|
+
let result;
|
|
1295
|
+
if (typeof url === "string") {
|
|
1296
|
+
const remote = await probeRemoteMcpServer(serverName, url, {
|
|
1297
|
+
allowNetwork: options.allowNetwork,
|
|
1298
|
+
allowLocalNetwork: options.allowLocalNetwork,
|
|
1299
|
+
requestTimeoutMs: options.remoteRequestTimeoutMs,
|
|
1300
|
+
lookup: options.remoteLookup,
|
|
1301
|
+
request: options.remoteRequest
|
|
1297
1302
|
});
|
|
1303
|
+
const remoteScorecard = createRuntimeScorecard();
|
|
1304
|
+
remoteScorecard.remote = remote.scorecard;
|
|
1305
|
+
result = {
|
|
1306
|
+
findings: remote.findings.map((finding) => withRuntimeEvidence(finding, serverName)),
|
|
1307
|
+
scorecard: remoteScorecard
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
else {
|
|
1311
|
+
const command = config.command;
|
|
1312
|
+
if (typeof command !== "string") {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const args = Array.isArray(config.args)
|
|
1316
|
+
? config.args.filter((value) => typeof value === "string")
|
|
1317
|
+
: [];
|
|
1318
|
+
const cwd = await resolveRuntimeCwd(canonicalRootPath, config.cwd);
|
|
1319
|
+
result = cwd === null
|
|
1320
|
+
? (() => {
|
|
1321
|
+
const invalidCwdScorecard = createRuntimeScorecard();
|
|
1322
|
+
invalidCwdScorecard.initialize = "fail";
|
|
1323
|
+
return {
|
|
1324
|
+
findings: [
|
|
1325
|
+
withRuntimeEvidence(buildFailure("plugin.runtime.startup.invalid_cwd", `The MCP server \`${serverName}\` has an invalid runtime working directory.`, "Runtime validation must not start a server from a missing, non-directory, or out-of-package working directory.", "Set the MCP server cwd to an existing directory inside the plugin package root, or remove it."), serverName)
|
|
1326
|
+
],
|
|
1327
|
+
scorecard: invalidCwdScorecard
|
|
1328
|
+
};
|
|
1329
|
+
})()
|
|
1330
|
+
: await probeCommandServer({
|
|
1331
|
+
serverName,
|
|
1332
|
+
packageRoot: canonicalRootPath,
|
|
1333
|
+
command,
|
|
1334
|
+
args,
|
|
1335
|
+
cwd,
|
|
1336
|
+
startupTimeoutMs,
|
|
1337
|
+
sandbox: options.sandbox,
|
|
1338
|
+
transcript: options.transcript
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1298
1341
|
scorecard = hasProbedServer
|
|
1299
1342
|
? mergeRuntimeScorecards(scorecard, result.scorecard)
|
|
1300
1343
|
: result.scorecard;
|
|
@@ -2,6 +2,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
4
4
|
import { discoverPackage } from "./discover-package.js";
|
|
5
|
+
import { inspectRemoteMcpUrl } from "./remote-url-policy.js";
|
|
5
6
|
import { probeRuntime } from "./runtime-probe.js";
|
|
6
7
|
function buildFailure(id, message, impact, suggestedFix, evidence) {
|
|
7
8
|
return {
|
|
@@ -23,6 +24,16 @@ function buildWarning(id, message, impact, suggestedFix, evidence) {
|
|
|
23
24
|
...(evidence ? { evidence } : {})
|
|
24
25
|
};
|
|
25
26
|
}
|
|
27
|
+
function remoteUrlIssueFindingId(issue) {
|
|
28
|
+
return issue === "insecure_non_loopback"
|
|
29
|
+
? "plugin.security.insecure_http_url"
|
|
30
|
+
: `plugin.security.remote_mcp_url.${issue}`;
|
|
31
|
+
}
|
|
32
|
+
function remoteUrlIssueMessage(issue) {
|
|
33
|
+
return issue === "insecure_non_loopback"
|
|
34
|
+
? "uses an insecure public HTTP URL"
|
|
35
|
+
: `uses a remote MCP URL with ${issue.replaceAll("_", " ")}`;
|
|
36
|
+
}
|
|
26
37
|
async function directoryExists(targetPath) {
|
|
27
38
|
try {
|
|
28
39
|
const details = await stat(targetPath);
|
|
@@ -376,6 +387,24 @@ async function validateMcpConfig(discoveredPackage) {
|
|
|
376
387
|
field: "transport"
|
|
377
388
|
}));
|
|
378
389
|
}
|
|
390
|
+
if (typeof command === "string" && typeof url === "string") {
|
|
391
|
+
findings.push(buildFailure("plugin.mcp.server.transport.conflict", `The MCP server \`${serverName}\` must not define both \`command\` and \`url\`.`, "A server with two transports cannot be selected deterministically by MCP clients.", `Keep either \`command\` or \`url\` for the \`${serverName}\` entry in \`${mcpConfigPath}\`.`, {
|
|
392
|
+
configPath: relativePackagePath(rootPath, mcpConfigPath),
|
|
393
|
+
serverName,
|
|
394
|
+
field: "transport"
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
if (typeof url === "string") {
|
|
398
|
+
const inspection = inspectRemoteMcpUrl(url);
|
|
399
|
+
for (const issue of inspection.issues) {
|
|
400
|
+
findings.push(buildFailure(remoteUrlIssueFindingId(issue), `The MCP server \`${serverName}\` ${remoteUrlIssueMessage(issue)}.`, "Unsafe or ambiguous remote transport configuration can expose credentials or prevent reliable MCP connectivity.", "Use an absolute HTTPS URL without credentials, query parameters, fragments, or numeric IP literals; HTTP is only supported for localhost development.", {
|
|
401
|
+
configPath: relativePackagePath(rootPath, mcpConfigPath),
|
|
402
|
+
serverName,
|
|
403
|
+
field: "url",
|
|
404
|
+
url: inspection.sanitizedUrl
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
379
408
|
if (isPlainObject(env)) {
|
|
380
409
|
for (const [envKey, envValue] of Object.entries(env)) {
|
|
381
410
|
if (isSensitiveKey(envKey) &&
|
|
@@ -415,7 +444,9 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
415
444
|
? await probeRuntime(discoveredPackage, {
|
|
416
445
|
startupTimeoutMs: options.runtimeStartupTimeoutMs,
|
|
417
446
|
sandbox: options.runtimeSandbox,
|
|
418
|
-
transcript: options.runtimeTranscript
|
|
447
|
+
transcript: options.runtimeTranscript,
|
|
448
|
+
allowNetwork: options.allowNetwork,
|
|
449
|
+
allowLocalNetwork: options.allowLocalNetwork
|
|
419
450
|
})
|
|
420
451
|
: null;
|
|
421
452
|
const findings = [
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface BaselineSummary {
|
|
|
41
41
|
}
|
|
42
42
|
export interface CheckOptions {
|
|
43
43
|
runtime?: boolean;
|
|
44
|
+
allowNetwork?: boolean;
|
|
45
|
+
allowLocalNetwork?: boolean;
|
|
44
46
|
runtimeTranscript?: (line: string) => void;
|
|
45
47
|
runtimeStartupTimeoutMs?: number;
|
|
46
48
|
runtimeSandbox?: RuntimeSandboxMode;
|
|
@@ -91,6 +93,17 @@ export interface RuntimeScorecard {
|
|
|
91
93
|
promptsList: RuntimeCapabilityStatus;
|
|
92
94
|
promptGet: RuntimeCapabilityStatus;
|
|
93
95
|
conformance?: RuntimeConformanceScorecard;
|
|
96
|
+
remote?: RemoteRuntimeScorecard;
|
|
97
|
+
}
|
|
98
|
+
export interface RemoteRuntimeScorecard {
|
|
99
|
+
transport: RuntimeCapabilityStatus;
|
|
100
|
+
networkSafety: RuntimeCapabilityStatus;
|
|
101
|
+
initialize: RuntimeCapabilityStatus;
|
|
102
|
+
contentType: RuntimeCapabilityStatus;
|
|
103
|
+
session: "absent" | "present-valid" | "present-invalid";
|
|
104
|
+
protocolHeaders: RuntimeCapabilityStatus;
|
|
105
|
+
authorization: RuntimeCapabilityStatus;
|
|
106
|
+
overall: "pass" | "warn" | "fail" | "skipped";
|
|
94
107
|
}
|
|
95
108
|
export type McpConformanceProfile = "legacy" | "2025-11-25" | "future-compatible";
|
|
96
109
|
export interface RuntimeConformanceScorecard {
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnal
|
|
|
18
18
|
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
|
|
19
19
|
export { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed, type DoctorRuntimePlan, type RuntimeApprovalReport, type RuntimePlanServer } from "./core/runtime-plan.js";
|
|
20
20
|
export { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson, type DoctorRuntimePolicyReport, type RuntimePolicyDecision, type RuntimePolicyRecommendation } from "./core/runtime-policy.js";
|
|
21
|
+
export { inspectRemoteMcpUrl, type RemoteUrlInspection } from "./core/remote-url-policy.js";
|
|
22
|
+
export { resolveRemoteTarget, RemoteNetworkPolicyError, type ResolvedRemoteTarget, type ResolveRemoteTargetOptions } from "./core/remote-network-policy.js";
|
|
23
|
+
export { probeRemoteMcpServer, type RemoteMcpProbeOptions, type RemoteMcpProbeResult } from "./core/remote-mcp-probe.js";
|
|
24
|
+
export { checkRemoteOAuthReadiness, type RemoteOAuthReadinessOptions, type RemoteOAuthReadinessResult } from "./core/remote-oauth-readiness.js";
|
|
21
25
|
export { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBundle, renderDoctorReviewBundleDiff, renderDoctorReviewBundleDiffJson, renderDoctorReviewBundleJson, renderDoctorReviewBundleVerification, renderDoctorReviewBundleVerificationJson, verifyDoctorReviewBundle, type BuildDoctorReviewBundleOptions, type DoctorReviewBundle, type DoctorReviewBundleDiffChange, type DoctorReviewBundleDiffReport, type DoctorReviewBundleDiffSnapshot, type DoctorReviewBundleManifest, type DoctorReviewBundleVerificationReport } from "./core/review-bundle.js";
|
|
22
26
|
export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence, type BuildDoctorReleaseEvidenceOptions, type DoctorReleaseEvidenceAssetReport, type DoctorReleaseEvidenceGitMetadata, type DoctorReleaseEvidencePackageMetadata, type DoctorReleaseEvidenceReport, type DoctorReleaseEvidenceVerificationReport } from "./core/release-evidence.js";
|
|
23
27
|
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,10 @@ export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnal
|
|
|
18
18
|
export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
19
19
|
export { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
|
|
20
20
|
export { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson } from "./core/runtime-policy.js";
|
|
21
|
+
export { inspectRemoteMcpUrl } from "./core/remote-url-policy.js";
|
|
22
|
+
export { resolveRemoteTarget, RemoteNetworkPolicyError } from "./core/remote-network-policy.js";
|
|
23
|
+
export { probeRemoteMcpServer } from "./core/remote-mcp-probe.js";
|
|
24
|
+
export { checkRemoteOAuthReadiness } from "./core/remote-oauth-readiness.js";
|
|
21
25
|
export { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBundle, renderDoctorReviewBundleDiff, renderDoctorReviewBundleDiffJson, renderDoctorReviewBundleJson, renderDoctorReviewBundleVerification, renderDoctorReviewBundleVerificationJson, verifyDoctorReviewBundle } from "./core/review-bundle.js";
|
|
22
26
|
export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
|
|
23
27
|
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
@@ -15,6 +15,8 @@ export interface GenericMcpDoctorReport {
|
|
|
15
15
|
}
|
|
16
16
|
export interface GenericMcpDoctorOptions {
|
|
17
17
|
runtime?: boolean;
|
|
18
|
+
allowNetwork?: boolean;
|
|
19
|
+
allowLocalNetwork?: boolean;
|
|
18
20
|
runtimeStartupTimeoutMs?: number;
|
|
19
21
|
}
|
|
20
22
|
export declare function buildGenericMcpDoctor(targetPath: string, environment?: CompatibilityEnvironment, options?: GenericMcpDoctorOptions): Promise<GenericMcpDoctorReport>;
|
|
@@ -64,9 +64,14 @@ function buildStaticMcpFindings(configPath, parsedConfig) {
|
|
|
64
64
|
findings.push(buildFinding("fail", "mcp.server.invalid", `The MCP server \`${serverName}\` must be configured as an object.`, "MCP clients cannot interpret a server entry unless it is represented as an object with server options.", `Change the \`${serverName}\` entry in \`${configPath}\` to an object.`, { configPath, serverName, field: "server" }));
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
const command = serverConfig.command;
|
|
68
|
+
const url = serverConfig.url;
|
|
69
|
+
if (typeof command !== "string" && typeof url !== "string") {
|
|
68
70
|
findings.push(buildFinding("fail", "mcp.server.transport.missing", `The MCP server \`${serverName}\` must define either \`command\` or \`url\`.`, "MCP clients need a process command for stdio servers or a URL for remote servers.", `Add either \`command\` or \`url\` to the \`${serverName}\` entry in \`${configPath}\`.`, { configPath, serverName, field: "transport" }));
|
|
69
71
|
}
|
|
72
|
+
if (typeof command === "string" && typeof url === "string") {
|
|
73
|
+
findings.push(buildFinding("fail", "mcp.server.transport.conflict", `The MCP server \`${serverName}\` must not define both \`command\` and \`url\`.`, "A server with two transports cannot be selected deterministically by MCP clients.", `Keep either \`command\` or \`url\` for the \`${serverName}\` entry in \`${configPath}\`.`, { configPath, serverName, field: "transport" }));
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
return {
|
|
72
77
|
serverCount: Object.keys(servers).length,
|
|
@@ -124,7 +129,7 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
const security = buildSecurityAuditFromFindings(rootPath, mcpConfigPath && parsedConfig !== null
|
|
127
|
-
? auditMcpServerConfig(rootPath, parsedConfig)
|
|
132
|
+
? auditMcpServerConfig(rootPath, parsedConfig, { configPath: mcpConfigPath })
|
|
128
133
|
: []);
|
|
129
134
|
const runtimeResult = options.runtime &&
|
|
130
135
|
mcpConfigPath !== null &&
|
|
@@ -135,7 +140,9 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
|
|
|
135
140
|
!staticFindings.some((finding) => finding.severity === "fail") &&
|
|
136
141
|
security.status !== "fail"
|
|
137
142
|
? await probeRuntimeConfig(canonicalRootPath, canonicalMcpConfigPath, {
|
|
138
|
-
startupTimeoutMs: options.runtimeStartupTimeoutMs
|
|
143
|
+
startupTimeoutMs: options.runtimeStartupTimeoutMs,
|
|
144
|
+
allowNetwork: options.allowNetwork,
|
|
145
|
+
allowLocalNetwork: options.allowLocalNetwork
|
|
139
146
|
})
|
|
140
147
|
: null;
|
|
141
148
|
const fingerprintedFindings = withFindingFingerprints([...staticFindings, ...(runtimeResult?.findings ?? [])], rootPath);
|
|
@@ -17,19 +17,32 @@ function appendRuntimeScorecard(lines, result) {
|
|
|
17
17
|
lines.push(`| prompts/list | ${result.runtimeScorecard.promptsList.toUpperCase()} |`);
|
|
18
18
|
lines.push(`| prompts/get | ${result.runtimeScorecard.promptGet.toUpperCase()} |`);
|
|
19
19
|
const conformance = result.runtimeScorecard.conformance;
|
|
20
|
-
if (
|
|
21
|
-
|
|
20
|
+
if (conformance) {
|
|
21
|
+
lines.push("", "## MCP Conformance", "");
|
|
22
|
+
lines.push("| Check | Status |");
|
|
23
|
+
lines.push("| --- | --- |");
|
|
24
|
+
lines.push(`| Protocol version | ${conformance.protocolVersion ?? "unavailable"} |`);
|
|
25
|
+
lines.push(`| Profile | ${conformance.profile ?? "unavailable"} |`);
|
|
26
|
+
lines.push(`| Capability consistency | ${conformance.capabilityConsistency.toUpperCase()} |`);
|
|
27
|
+
lines.push(`| Task declarations | ${conformance.taskDeclarations.toUpperCase()} |`);
|
|
28
|
+
lines.push(`| Tasks list | ${conformance.tasksList.toUpperCase()} |`);
|
|
29
|
+
lines.push(`| Schema dialect | ${conformance.schemaDialect.toUpperCase()} |`);
|
|
30
|
+
lines.push(`| Overall | ${conformance.overall.toUpperCase()} |`);
|
|
31
|
+
}
|
|
32
|
+
const remote = result.runtimeScorecard.remote;
|
|
33
|
+
if (remote) {
|
|
34
|
+
lines.push("", "## Remote MCP Scorecard", "");
|
|
35
|
+
lines.push("| Check | Status |");
|
|
36
|
+
lines.push("| --- | --- |");
|
|
37
|
+
lines.push(`| Transport | ${remote.transport.toUpperCase()} |`);
|
|
38
|
+
lines.push(`| Network safety | ${remote.networkSafety.toUpperCase()} |`);
|
|
39
|
+
lines.push(`| Initialize | ${remote.initialize.toUpperCase()} |`);
|
|
40
|
+
lines.push(`| Content type | ${remote.contentType.toUpperCase()} |`);
|
|
41
|
+
lines.push(`| Session | ${remote.session.toUpperCase()} |`);
|
|
42
|
+
lines.push(`| Protocol headers | ${remote.protocolHeaders.toUpperCase()} |`);
|
|
43
|
+
lines.push(`| Authorization | ${remote.authorization.toUpperCase()} |`);
|
|
44
|
+
lines.push(`| Overall | ${remote.overall.toUpperCase()} |`);
|
|
22
45
|
}
|
|
23
|
-
lines.push("", "## MCP Conformance", "");
|
|
24
|
-
lines.push("| Check | Status |");
|
|
25
|
-
lines.push("| --- | --- |");
|
|
26
|
-
lines.push(`| Protocol version | ${conformance.protocolVersion ?? "unavailable"} |`);
|
|
27
|
-
lines.push(`| Profile | ${conformance.profile ?? "unavailable"} |`);
|
|
28
|
-
lines.push(`| Capability consistency | ${conformance.capabilityConsistency.toUpperCase()} |`);
|
|
29
|
-
lines.push(`| Task declarations | ${conformance.taskDeclarations.toUpperCase()} |`);
|
|
30
|
-
lines.push(`| Tasks list | ${conformance.tasksList.toUpperCase()} |`);
|
|
31
|
-
lines.push(`| Schema dialect | ${conformance.schemaDialect.toUpperCase()} |`);
|
|
32
|
-
lines.push(`| Overall | ${conformance.overall.toUpperCase()} |`);
|
|
33
46
|
}
|
|
34
47
|
export function buildMarkdownReport(result, options) {
|
|
35
48
|
const failCount = result.findings.filter((finding) => finding.severity === "fail").length;
|
|
@@ -38,17 +38,28 @@ function appendRuntimeScorecard(lines, result) {
|
|
|
38
38
|
lines.push(`prompts/list: ${result.runtimeScorecard.promptsList}`);
|
|
39
39
|
lines.push(`prompts/get: ${result.runtimeScorecard.promptGet}`);
|
|
40
40
|
const conformance = result.runtimeScorecard.conformance;
|
|
41
|
-
if (
|
|
42
|
-
|
|
41
|
+
if (conformance) {
|
|
42
|
+
lines.push("", "MCP Conformance", "---------------");
|
|
43
|
+
lines.push(`Protocol version: ${conformance.protocolVersion ?? "unavailable"}`);
|
|
44
|
+
lines.push(`Profile: ${conformance.profile ?? "unavailable"}`);
|
|
45
|
+
lines.push(`Capability consistency: ${conformance.capabilityConsistency}`);
|
|
46
|
+
lines.push(`Task declarations: ${conformance.taskDeclarations}`);
|
|
47
|
+
lines.push(`Tasks list: ${conformance.tasksList}`);
|
|
48
|
+
lines.push(`Schema dialect: ${conformance.schemaDialect}`);
|
|
49
|
+
lines.push(`Overall: ${conformance.overall}`);
|
|
50
|
+
}
|
|
51
|
+
const remote = result.runtimeScorecard.remote;
|
|
52
|
+
if (remote) {
|
|
53
|
+
lines.push("", "Remote MCP Scorecard", "--------------------");
|
|
54
|
+
lines.push(`transport: ${remote.transport}`);
|
|
55
|
+
lines.push(`network safety: ${remote.networkSafety}`);
|
|
56
|
+
lines.push(`initialize: ${remote.initialize}`);
|
|
57
|
+
lines.push(`content type: ${remote.contentType}`);
|
|
58
|
+
lines.push(`session: ${remote.session}`);
|
|
59
|
+
lines.push(`protocol headers: ${remote.protocolHeaders}`);
|
|
60
|
+
lines.push(`authorization: ${remote.authorization}`);
|
|
61
|
+
lines.push(`overall: ${remote.overall}`);
|
|
43
62
|
}
|
|
44
|
-
lines.push("", "MCP Conformance", "---------------");
|
|
45
|
-
lines.push(`Protocol version: ${conformance.protocolVersion ?? "unavailable"}`);
|
|
46
|
-
lines.push(`Profile: ${conformance.profile ?? "unavailable"}`);
|
|
47
|
-
lines.push(`Capability consistency: ${conformance.capabilityConsistency}`);
|
|
48
|
-
lines.push(`Task declarations: ${conformance.taskDeclarations}`);
|
|
49
|
-
lines.push(`Tasks list: ${conformance.tasksList}`);
|
|
50
|
-
lines.push(`Schema dialect: ${conformance.schemaDialect}`);
|
|
51
|
-
lines.push(`Overall: ${conformance.overall}`);
|
|
52
63
|
}
|
|
53
64
|
export function renderTextReport(result, options = {}) {
|
|
54
65
|
const ascii = options.ascii ?? false;
|