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.
@@ -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
- if (plan.summary.executableServerCount === 0) {
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 command = config.command;
1270
- if (typeof command !== "string") {
1271
- continue;
1272
- }
1273
- const args = Array.isArray(config.args)
1274
- ? config.args.filter((value) => typeof value === "string")
1275
- : [];
1276
- const cwd = await resolveRuntimeCwd(canonicalRootPath, config.cwd);
1277
- const result = cwd === null
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 = [
@@ -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
- if (typeof serverConfig.command !== "string" && typeof serverConfig.url !== "string") {
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 (!conformance) {
21
- return;
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 (!conformance) {
42
- return;
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;