codex-plugin-doctor 1.49.0 → 1.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +3 -3
- package/dist/core/mcp-conformance.d.ts +2 -0
- package/dist/core/mcp-conformance.js +215 -0
- package/dist/core/npm-package-doctor.d.ts +11 -0
- package/dist/core/npm-package-doctor.js +12 -2
- package/dist/core/output-contract.js +82 -5
- package/dist/core/runtime-plan.js +2 -0
- package/dist/core/runtime-probe.d.ts +4 -2
- package/dist/core/runtime-probe.js +326 -64
- package/dist/core/runtime-transcript.js +13 -3
- package/dist/domain/types.d.ts +34 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +8 -2
- package/dist/mcp/generic-mcp-doctor.js +35 -11
- package/dist/reporting/render-markdown-report.js +31 -13
- package/dist/reporting/render-text-report.js +28 -22
- package/dist/rules/rule-catalog.js +72 -0
- package/dist/run-cli.js +43 -8
- package/examples/README.md +94 -94
- package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
- package/examples/codex-doctor-generic-mcp/server.js +1 -1
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-risky/.mcp.json +12 -12
- package/examples/codex-doctor-risky/mock-server.js +1 -1
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
- package/examples/codex-doctor-runtime/.mcp.json +9 -9
- package/examples/codex-doctor-runtime/mock-server.js +237 -237
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import readline from "node:readline";
|
|
6
|
+
import { evaluateMcpConformance } from "./mcp-conformance.js";
|
|
6
7
|
import { buildRuntimeLaunch, DOCKER_RUNTIME_STARTUP_TIMEOUT_MS, RuntimeSandboxError } from "./runtime-sandbox.js";
|
|
7
8
|
import { formatRequestTranscript as formatRequestTranscriptForLog, formatResponseTranscript as formatResponseTranscriptForLog } from "./runtime-transcript.js";
|
|
8
9
|
const MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
@@ -12,6 +13,7 @@ const MAX_TOOL_CALL_CONTENT_LENGTH = 4096;
|
|
|
12
13
|
const MAX_RESOURCE_READ_CONTENT_LENGTH = 4096;
|
|
13
14
|
const MAX_PROMPT_GET_CONTENT_LENGTH = 4096;
|
|
14
15
|
const DOCKER_CLEANUP_TIMEOUT_MS = 5_000;
|
|
16
|
+
const MAX_RUNTIME_PAGINATION_PAGES = 100;
|
|
15
17
|
function buildNativeRuntimeLaunch(input) {
|
|
16
18
|
return {
|
|
17
19
|
...input,
|
|
@@ -33,7 +35,16 @@ function createRuntimeScorecard() {
|
|
|
33
35
|
resourceRead: "unsupported",
|
|
34
36
|
resourceTemplatesList: "unsupported",
|
|
35
37
|
promptsList: "unsupported",
|
|
36
|
-
promptGet: "unsupported"
|
|
38
|
+
promptGet: "unsupported",
|
|
39
|
+
conformance: {
|
|
40
|
+
protocolVersion: null,
|
|
41
|
+
profile: null,
|
|
42
|
+
capabilityConsistency: "skipped",
|
|
43
|
+
taskDeclarations: "skipped",
|
|
44
|
+
tasksList: "skipped",
|
|
45
|
+
schemaDialect: "skipped",
|
|
46
|
+
overall: "skipped"
|
|
47
|
+
}
|
|
37
48
|
};
|
|
38
49
|
}
|
|
39
50
|
function buildFailure(id, message, impact, suggestedFix, evidence) {
|
|
@@ -57,6 +68,16 @@ function buildWarning(id, message, impact, suggestedFix, evidence) {
|
|
|
57
68
|
};
|
|
58
69
|
}
|
|
59
70
|
function methodForRuntimeFinding(id) {
|
|
71
|
+
if (id.startsWith("mcp.conformance.protocol.") ||
|
|
72
|
+
id.startsWith("mcp.conformance.tasks.capability_")) {
|
|
73
|
+
return "initialize";
|
|
74
|
+
}
|
|
75
|
+
if (id.startsWith("mcp.conformance.tasks.task_support_") ||
|
|
76
|
+
id.startsWith("mcp.conformance.schema.")) {
|
|
77
|
+
return "tools/list";
|
|
78
|
+
}
|
|
79
|
+
if (id.startsWith("mcp.conformance.tasks_list."))
|
|
80
|
+
return "tasks/list";
|
|
60
81
|
if (id.includes(".initialize."))
|
|
61
82
|
return "initialize";
|
|
62
83
|
if (id.includes(".tools_list."))
|
|
@@ -81,9 +102,9 @@ function withRuntimeEvidence(finding, serverName) {
|
|
|
81
102
|
return {
|
|
82
103
|
...finding,
|
|
83
104
|
evidence: {
|
|
105
|
+
...(finding.evidence ?? {}),
|
|
84
106
|
serverName,
|
|
85
|
-
method: methodForRuntimeFinding(finding.id)
|
|
86
|
-
...(finding.evidence ?? {})
|
|
107
|
+
method: methodForRuntimeFinding(finding.id)
|
|
87
108
|
}
|
|
88
109
|
};
|
|
89
110
|
}
|
|
@@ -228,15 +249,7 @@ function isPathWithinRoot(rootPath, candidatePath) {
|
|
|
228
249
|
return (relativePath === "" ||
|
|
229
250
|
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
|
|
230
251
|
}
|
|
231
|
-
async function loadMcpServers(
|
|
232
|
-
const { manifest, rootPath } = discoveredPackage;
|
|
233
|
-
if (!manifest.mcpServers) {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
|
|
237
|
-
if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
252
|
+
async function loadMcpServers(mcpConfigPath) {
|
|
240
253
|
const exists = await fileExists(mcpConfigPath);
|
|
241
254
|
if (!exists) {
|
|
242
255
|
return null;
|
|
@@ -276,6 +289,57 @@ function hasPromptsCapability(message) {
|
|
|
276
289
|
const capabilities = getCapabilities(message);
|
|
277
290
|
return capabilities !== null && isPlainObject(capabilities.prompts);
|
|
278
291
|
}
|
|
292
|
+
function worstRuntimeStatus(left, right) {
|
|
293
|
+
const severity = {
|
|
294
|
+
fail: 4,
|
|
295
|
+
warn: 3,
|
|
296
|
+
pass: 2,
|
|
297
|
+
skipped: 1,
|
|
298
|
+
unsupported: 0
|
|
299
|
+
};
|
|
300
|
+
return severity[left] >= severity[right] ? left : right;
|
|
301
|
+
}
|
|
302
|
+
function mergeRuntimeScorecards(left, right) {
|
|
303
|
+
const leftConformance = left.conformance;
|
|
304
|
+
const rightConformance = right.conformance;
|
|
305
|
+
const conformance = leftConformance && rightConformance
|
|
306
|
+
? {
|
|
307
|
+
protocolVersion: leftConformance.protocolVersion === null
|
|
308
|
+
? rightConformance.protocolVersion
|
|
309
|
+
: rightConformance.protocolVersion === null ||
|
|
310
|
+
leftConformance.protocolVersion === rightConformance.protocolVersion
|
|
311
|
+
? leftConformance.protocolVersion
|
|
312
|
+
: null,
|
|
313
|
+
profile: leftConformance.profile === null
|
|
314
|
+
? rightConformance.profile
|
|
315
|
+
: rightConformance.profile === null || leftConformance.profile === rightConformance.profile
|
|
316
|
+
? leftConformance.profile
|
|
317
|
+
: null,
|
|
318
|
+
capabilityConsistency: worstRuntimeStatus(leftConformance.capabilityConsistency, rightConformance.capabilityConsistency),
|
|
319
|
+
taskDeclarations: worstRuntimeStatus(leftConformance.taskDeclarations, rightConformance.taskDeclarations),
|
|
320
|
+
tasksList: worstRuntimeStatus(leftConformance.tasksList, rightConformance.tasksList),
|
|
321
|
+
schemaDialect: worstRuntimeStatus(leftConformance.schemaDialect, rightConformance.schemaDialect),
|
|
322
|
+
overall: worstRuntimeStatus(leftConformance.overall, rightConformance.overall)
|
|
323
|
+
}
|
|
324
|
+
: leftConformance ?? rightConformance;
|
|
325
|
+
return {
|
|
326
|
+
initialize: worstRuntimeStatus(left.initialize, right.initialize),
|
|
327
|
+
toolsList: worstRuntimeStatus(left.toolsList, right.toolsList),
|
|
328
|
+
toolsCall: worstRuntimeStatus(left.toolsCall, right.toolsCall),
|
|
329
|
+
resourcesList: worstRuntimeStatus(left.resourcesList, right.resourcesList),
|
|
330
|
+
resourceRead: worstRuntimeStatus(left.resourceRead, right.resourceRead),
|
|
331
|
+
resourceTemplatesList: worstRuntimeStatus(left.resourceTemplatesList, right.resourceTemplatesList),
|
|
332
|
+
promptsList: worstRuntimeStatus(left.promptsList, right.promptsList),
|
|
333
|
+
promptGet: worstRuntimeStatus(left.promptGet, right.promptGet),
|
|
334
|
+
...(conformance ? { conformance } : {})
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function supportsTasksListProbe(protocolVersion, capabilities) {
|
|
338
|
+
if (protocolVersion < MCP_PROTOCOL_VERSION || !isPlainObject(capabilities.tasks)) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
return isPlainObject(capabilities.tasks.list);
|
|
342
|
+
}
|
|
279
343
|
function getNextCursor(result) {
|
|
280
344
|
if (result.nextCursor === undefined) {
|
|
281
345
|
return null;
|
|
@@ -294,13 +358,14 @@ function extractToolsPage(message) {
|
|
|
294
358
|
for (const tool of tools) {
|
|
295
359
|
if (!isPlainObject(tool) ||
|
|
296
360
|
typeof tool.name !== "string" ||
|
|
297
|
-
!
|
|
298
|
-
tool.inputSchema.type !== "object") {
|
|
361
|
+
!("inputSchema" in tool)) {
|
|
299
362
|
return null;
|
|
300
363
|
}
|
|
301
364
|
parsedTools.push({
|
|
302
365
|
name: tool.name,
|
|
303
|
-
inputSchema: tool.inputSchema
|
|
366
|
+
inputSchema: tool.inputSchema,
|
|
367
|
+
...(tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }),
|
|
368
|
+
...(tool.execution === undefined ? {} : { execution: tool.execution })
|
|
304
369
|
});
|
|
305
370
|
}
|
|
306
371
|
const nextCursor = getNextCursor(message.result);
|
|
@@ -472,16 +537,55 @@ function findCallableTool(tools) {
|
|
|
472
537
|
if (isDestructiveTool(tool)) {
|
|
473
538
|
continue;
|
|
474
539
|
}
|
|
475
|
-
|
|
540
|
+
if (!isPlainObject(tool.inputSchema) || tool.inputSchema.type !== "object") {
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const callableTool = {
|
|
544
|
+
name: tool.name,
|
|
545
|
+
inputSchema: tool.inputSchema
|
|
546
|
+
};
|
|
547
|
+
const args = buildToolArguments(callableTool);
|
|
476
548
|
if (args !== undefined) {
|
|
477
549
|
return {
|
|
478
|
-
tool,
|
|
550
|
+
tool: callableTool,
|
|
479
551
|
args
|
|
480
552
|
};
|
|
481
553
|
}
|
|
482
554
|
}
|
|
483
555
|
return null;
|
|
484
556
|
}
|
|
557
|
+
function hasValidToolInputSchemas(tools) {
|
|
558
|
+
return tools.every((tool) => isPlainObject(tool.inputSchema) && tool.inputSchema.type === "object");
|
|
559
|
+
}
|
|
560
|
+
async function resolveRuntimeCwd(rootPath, configuredCwd) {
|
|
561
|
+
const resolvedCwd = typeof configuredCwd === "string"
|
|
562
|
+
? path.resolve(rootPath, configuredCwd)
|
|
563
|
+
: rootPath;
|
|
564
|
+
try {
|
|
565
|
+
const [canonicalCwd, details] = await Promise.all([
|
|
566
|
+
realpath(resolvedCwd),
|
|
567
|
+
stat(resolvedCwd)
|
|
568
|
+
]);
|
|
569
|
+
return details.isDirectory() && isPathWithinRoot(rootPath, canonicalCwd)
|
|
570
|
+
? canonicalCwd
|
|
571
|
+
: null;
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function isProtocolVersion(value) {
|
|
578
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
579
|
+
if (!match) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
583
|
+
const date = new Date(0);
|
|
584
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
585
|
+
return (date.getUTCFullYear() === year &&
|
|
586
|
+
date.getUTCMonth() === month - 1 &&
|
|
587
|
+
date.getUTCDate() === day);
|
|
588
|
+
}
|
|
485
589
|
function buildPromptArguments(prompt) {
|
|
486
590
|
if (!prompt.arguments || prompt.arguments.length === 0) {
|
|
487
591
|
return undefined;
|
|
@@ -744,7 +848,12 @@ async function probeCommandServer(input) {
|
|
|
744
848
|
const fetchPaginated = async (input) => {
|
|
745
849
|
let cursor = null;
|
|
746
850
|
const items = [];
|
|
851
|
+
const seenCursors = new Set();
|
|
852
|
+
let pageCount = 0;
|
|
747
853
|
do {
|
|
854
|
+
if (pageCount >= MAX_RUNTIME_PAGINATION_PAGES) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
748
857
|
const response = await sendRequest(input.method, cursor ? { cursor } : undefined, input.timeoutFinding);
|
|
749
858
|
transcript?.(formatResponseTranscriptForLog(input.method, response));
|
|
750
859
|
if (isErrorResponse(response)) {
|
|
@@ -759,10 +868,81 @@ async function probeCommandServer(input) {
|
|
|
759
868
|
return null;
|
|
760
869
|
}
|
|
761
870
|
items.push(...page.items);
|
|
871
|
+
pageCount += 1;
|
|
872
|
+
if (page.nextCursor && seenCursors.has(page.nextCursor)) {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
if (page.nextCursor) {
|
|
876
|
+
seenCursors.add(page.nextCursor);
|
|
877
|
+
}
|
|
762
878
|
cursor = page.nextCursor;
|
|
763
879
|
} while (cursor);
|
|
764
880
|
return items;
|
|
765
881
|
};
|
|
882
|
+
const observeTasksList = async () => {
|
|
883
|
+
let cursor = null;
|
|
884
|
+
const seenCursors = new Set();
|
|
885
|
+
let itemCount = 0;
|
|
886
|
+
let pageCount = 0;
|
|
887
|
+
try {
|
|
888
|
+
do {
|
|
889
|
+
if (pageCount >= MAX_RUNTIME_PAGINATION_PAGES) {
|
|
890
|
+
return {
|
|
891
|
+
status: "fail",
|
|
892
|
+
itemCount,
|
|
893
|
+
pageCount,
|
|
894
|
+
failure: "invalid"
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const response = await sendRequest("tasks/list", cursor ? { cursor } : undefined, buildFailure("mcp.conformance.tasks_list.timeout", `The MCP server \`${serverName}\` did not answer the tasks/list request in time.`, "Codex cannot safely inspect task discovery when the server does not complete tasks/list.", "Reduce tasks/list latency and verify pagination completes."));
|
|
898
|
+
if (isErrorResponse(response) || !isPlainObject(response.result)) {
|
|
899
|
+
return {
|
|
900
|
+
status: "fail",
|
|
901
|
+
itemCount,
|
|
902
|
+
pageCount,
|
|
903
|
+
failure: "invalid"
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const tasks = response.result.tasks;
|
|
907
|
+
const nextCursor = getNextCursor(response.result);
|
|
908
|
+
if (!Array.isArray(tasks) ||
|
|
909
|
+
!tasks.every(isPlainObject) ||
|
|
910
|
+
nextCursor === "__invalid__") {
|
|
911
|
+
return {
|
|
912
|
+
status: "fail",
|
|
913
|
+
itemCount,
|
|
914
|
+
pageCount,
|
|
915
|
+
failure: "invalid"
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
itemCount += tasks.length;
|
|
919
|
+
pageCount += 1;
|
|
920
|
+
if (nextCursor && seenCursors.has(nextCursor)) {
|
|
921
|
+
return {
|
|
922
|
+
status: "fail",
|
|
923
|
+
itemCount,
|
|
924
|
+
pageCount,
|
|
925
|
+
failure: "invalid"
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (nextCursor) {
|
|
929
|
+
seenCursors.add(nextCursor);
|
|
930
|
+
}
|
|
931
|
+
cursor = nextCursor;
|
|
932
|
+
} while (cursor);
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
return {
|
|
936
|
+
status: "fail",
|
|
937
|
+
itemCount,
|
|
938
|
+
pageCount,
|
|
939
|
+
failure: isFinding(error) && error.id === "mcp.conformance.tasks_list.timeout"
|
|
940
|
+
? "timeout"
|
|
941
|
+
: "invalid"
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
return { status: "pass", itemCount, pageCount };
|
|
945
|
+
};
|
|
766
946
|
child.stderr?.on("data", (chunk) => {
|
|
767
947
|
if (stderrPreview.length >= 160) {
|
|
768
948
|
return;
|
|
@@ -835,6 +1015,7 @@ async function probeCommandServer(input) {
|
|
|
835
1015
|
}
|
|
836
1016
|
const result = initializeResponse.result;
|
|
837
1017
|
if (typeof result.protocolVersion !== "string" ||
|
|
1018
|
+
!isProtocolVersion(result.protocolVersion) ||
|
|
838
1019
|
!isPlainObject(result.capabilities) ||
|
|
839
1020
|
!isPlainObject(result.serverInfo) ||
|
|
840
1021
|
typeof result.serverInfo.name !== "string" ||
|
|
@@ -845,43 +1026,79 @@ async function probeCommandServer(input) {
|
|
|
845
1026
|
}
|
|
846
1027
|
scorecard.initialize = "pass";
|
|
847
1028
|
sendNotification("notifications/initialized");
|
|
1029
|
+
let tools = [];
|
|
1030
|
+
let toolTerminalFinding = null;
|
|
1031
|
+
let canEvaluateConformance = true;
|
|
848
1032
|
if (!hasToolsCapability(initializeResponse)) {
|
|
849
1033
|
scorecard.toolsList = "unsupported";
|
|
850
1034
|
scorecard.toolsCall = "unsupported";
|
|
851
|
-
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
const tools = await fetchPaginated({
|
|
855
|
-
method: "tools/list",
|
|
856
|
-
timeoutFinding: buildFailure("plugin.runtime.tools_list.timeout", `The MCP server \`${serverName}\` did not answer the tools/list request in time.`, "A server that cannot return its tool catalog in time will feel broken or invisible in Codex.", "Inspect the tool discovery path and reduce latency before returning the tool list."),
|
|
857
|
-
extractPage: extractToolsPage
|
|
858
|
-
});
|
|
859
|
-
if (!tools) {
|
|
860
|
-
scorecard.toolsList = "fail";
|
|
861
|
-
settle(buildFailure("plugin.runtime.tools_list.invalid", `The MCP server \`${serverName}\` returned an invalid tools/list result.`, "Codex cannot safely consume malformed tool definitions from `tools/list`.", "Return a `tools` array where every tool has a string `name` and an object-shaped `inputSchema` with `type: \"object\"`."));
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
scorecard.toolsList = "pass";
|
|
865
|
-
const callableTool = findCallableTool(tools);
|
|
866
|
-
if (!callableTool) {
|
|
867
|
-
scorecard.toolsCall = "skipped";
|
|
868
|
-
settle(buildWarning("plugin.runtime.tool_call.skipped", `The MCP server \`${serverName}\` does not expose a safely callable tool for probing.`, "The validator confirmed tool discovery but could not safely perform a non-destructive `tools/call` probe.", "Expose at least one non-destructive tool with a JSON schema the validator can generate arguments for."));
|
|
869
|
-
return;
|
|
1035
|
+
warnings.push(buildWarning("plugin.runtime.tools.unsupported", `The MCP server \`${serverName}\` does not advertise tools capability.`, "Codex cannot use `tools/list` for deeper validation if the server does not declare tools support.", "Expose `capabilities.tools` during initialize if this server is expected to provide tools."));
|
|
870
1036
|
}
|
|
871
1037
|
else {
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1038
|
+
const listedTools = await fetchPaginated({
|
|
1039
|
+
method: "tools/list",
|
|
1040
|
+
timeoutFinding: buildFailure("plugin.runtime.tools_list.timeout", `The MCP server \`${serverName}\` did not answer the tools/list request in time.`, "A server that cannot return its tool catalog in time will feel broken or invisible in Codex.", "Inspect the tool discovery path and reduce latency before returning the tool list."),
|
|
1041
|
+
extractPage: extractToolsPage
|
|
1042
|
+
});
|
|
1043
|
+
if (!listedTools) {
|
|
1044
|
+
canEvaluateConformance = false;
|
|
1045
|
+
scorecard.toolsList = "fail";
|
|
1046
|
+
toolTerminalFinding = buildFailure("plugin.runtime.tools_list.invalid", `The MCP server \`${serverName}\` returned an invalid tools/list result.`, "Codex cannot safely consume malformed tool definitions from `tools/list`.", "Return a `tools` array where every tool has a string `name` and an object-shaped `inputSchema` with `type: \"object\"`.");
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
tools = listedTools;
|
|
1050
|
+
if (!hasValidToolInputSchemas(tools)) {
|
|
1051
|
+
canEvaluateConformance = false;
|
|
1052
|
+
scorecard.toolsList = "fail";
|
|
1053
|
+
toolTerminalFinding = buildFailure("plugin.runtime.tools_list.invalid", `The MCP server \`${serverName}\` returned an invalid tools/list result.`, "Codex cannot safely consume malformed tool definitions from `tools/list`.", "Return a `tools` array where every tool has a string `name` and an object-shaped `inputSchema` with `type: \"object\"`.");
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
scorecard.toolsList = "pass";
|
|
1057
|
+
}
|
|
880
1058
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1059
|
+
const callableTool = toolTerminalFinding ? null : findCallableTool(tools);
|
|
1060
|
+
if (!toolTerminalFinding && !callableTool) {
|
|
1061
|
+
scorecard.toolsCall = "skipped";
|
|
1062
|
+
toolTerminalFinding = buildWarning("plugin.runtime.tool_call.skipped", `The MCP server \`${serverName}\` does not expose a safely callable tool for probing.`, "The validator confirmed tool discovery but could not safely perform a non-destructive `tools/call` probe.", "Expose at least one non-destructive tool with a JSON schema the validator can generate arguments for.");
|
|
1063
|
+
}
|
|
1064
|
+
else if (callableTool) {
|
|
1065
|
+
const toolCallResponse = await sendRequest("tools/call", {
|
|
1066
|
+
name: callableTool.tool.name,
|
|
1067
|
+
arguments: callableTool.args
|
|
1068
|
+
}, buildFailure("plugin.runtime.tool_call.timeout", `The MCP server \`${serverName}\` did not answer the tools/call request in time.`, "A server that cannot complete a basic tool invocation in time will feel broken in real Codex usage.", "Inspect the selected tool implementation and reduce its response latency.", { toolName: callableTool.tool.name }));
|
|
1069
|
+
if (isErrorResponse(toolCallResponse) || !isValidCallToolResult(toolCallResponse)) {
|
|
1070
|
+
scorecard.toolsCall = "fail";
|
|
1071
|
+
toolTerminalFinding = buildFailure("plugin.runtime.tool_call.invalid", `The MCP server \`${serverName}\` returned an invalid tools/call result.`, "Codex cannot safely consume malformed tool call results from the server.", "Return a CallToolResult with a `content` array containing valid MCP content blocks.", { toolName: callableTool.tool.name });
|
|
1072
|
+
}
|
|
1073
|
+
if (!toolTerminalFinding) {
|
|
1074
|
+
scorecard.toolsCall = "pass";
|
|
1075
|
+
warnings.push(...collectOversizedToolCallWarnings(toolCallResponse, {
|
|
1076
|
+
toolName: callableTool.tool.name
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const tasksList = supportsTasksListProbe(result.protocolVersion, result.capabilities)
|
|
1082
|
+
? await observeTasksList()
|
|
1083
|
+
: {
|
|
1084
|
+
status: "skipped",
|
|
1085
|
+
itemCount: 0,
|
|
1086
|
+
pageCount: 0
|
|
1087
|
+
};
|
|
1088
|
+
if (canEvaluateConformance || tasksList.status !== "skipped") {
|
|
1089
|
+
const conformance = evaluateMcpConformance({
|
|
1090
|
+
protocolVersion: result.protocolVersion,
|
|
1091
|
+
capabilities: result.capabilities,
|
|
1092
|
+
tools: tools,
|
|
1093
|
+
tasksList,
|
|
1094
|
+
skipToolDerivedChecks: !canEvaluateConformance
|
|
1095
|
+
});
|
|
1096
|
+
scorecard.conformance = conformance.scorecard;
|
|
1097
|
+
warnings.push(...conformance.findings);
|
|
1098
|
+
}
|
|
1099
|
+
if (toolTerminalFinding) {
|
|
1100
|
+
settle(toolTerminalFinding);
|
|
1101
|
+
return;
|
|
885
1102
|
}
|
|
886
1103
|
if (!hasResourcesCapability(initializeResponse)) {
|
|
887
1104
|
scorecard.resourcesList = "unsupported";
|
|
@@ -1001,8 +1218,40 @@ async function probeCommandServer(input) {
|
|
|
1001
1218
|
});
|
|
1002
1219
|
}
|
|
1003
1220
|
export async function probeRuntime(discoveredPackage, options = {}) {
|
|
1221
|
+
const { manifest, rootPath } = discoveredPackage;
|
|
1222
|
+
if (!manifest.mcpServers) {
|
|
1223
|
+
return {
|
|
1224
|
+
findings: [],
|
|
1225
|
+
scorecard: createRuntimeScorecard()
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
return probeRuntimeConfig(rootPath, manifest.mcpServers, options);
|
|
1229
|
+
}
|
|
1230
|
+
export async function probeRuntimeConfig(rootPath, mcpConfigPath, options = {}) {
|
|
1231
|
+
const resolvedRootPath = path.resolve(rootPath);
|
|
1232
|
+
const resolvedMcpConfigPath = path.resolve(resolvedRootPath, mcpConfigPath);
|
|
1004
1233
|
const startupTimeoutMs = options.startupTimeoutMs ?? 400;
|
|
1005
|
-
|
|
1234
|
+
let canonicalRootPath;
|
|
1235
|
+
let canonicalMcpConfigPath;
|
|
1236
|
+
try {
|
|
1237
|
+
[canonicalRootPath, canonicalMcpConfigPath] = await Promise.all([
|
|
1238
|
+
realpath(resolvedRootPath),
|
|
1239
|
+
realpath(resolvedMcpConfigPath)
|
|
1240
|
+
]);
|
|
1241
|
+
}
|
|
1242
|
+
catch {
|
|
1243
|
+
return {
|
|
1244
|
+
findings: [],
|
|
1245
|
+
scorecard: createRuntimeScorecard()
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (!isPathWithinRoot(canonicalRootPath, canonicalMcpConfigPath)) {
|
|
1249
|
+
return {
|
|
1250
|
+
findings: [],
|
|
1251
|
+
scorecard: createRuntimeScorecard()
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const servers = await loadMcpServers(canonicalMcpConfigPath);
|
|
1006
1255
|
if (!servers) {
|
|
1007
1256
|
return {
|
|
1008
1257
|
findings: [],
|
|
@@ -1012,6 +1261,7 @@ export async function probeRuntime(discoveredPackage, options = {}) {
|
|
|
1012
1261
|
const findings = [];
|
|
1013
1262
|
let scorecard = createRuntimeScorecard();
|
|
1014
1263
|
let execution;
|
|
1264
|
+
let hasProbedServer = false;
|
|
1015
1265
|
for (const [serverName, config] of Object.entries(servers)) {
|
|
1016
1266
|
if (!isPlainObject(config)) {
|
|
1017
1267
|
continue;
|
|
@@ -1023,20 +1273,32 @@ export async function probeRuntime(discoveredPackage, options = {}) {
|
|
|
1023
1273
|
const args = Array.isArray(config.args)
|
|
1024
1274
|
? config.args.filter((value) => typeof value === "string")
|
|
1025
1275
|
: [];
|
|
1026
|
-
const cwd =
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
|
1297
|
+
});
|
|
1298
|
+
scorecard = hasProbedServer
|
|
1299
|
+
? mergeRuntimeScorecards(scorecard, result.scorecard)
|
|
1300
|
+
: result.scorecard;
|
|
1301
|
+
hasProbedServer = true;
|
|
1040
1302
|
execution = result.execution ?? execution;
|
|
1041
1303
|
if (result.findings.length > 0) {
|
|
1042
1304
|
findings.push(...result.findings);
|
|
@@ -73,15 +73,20 @@ export function formatRequestTranscript(method, params) {
|
|
|
73
73
|
if (!params) {
|
|
74
74
|
return `-> ${method}`;
|
|
75
75
|
}
|
|
76
|
+
if (method === "tasks/list" && params.cursor !== undefined) {
|
|
77
|
+
return '-> tasks/list {"cursor":"[CURSOR]"}';
|
|
78
|
+
}
|
|
76
79
|
return `-> ${method} ${JSON.stringify(sanitizeTranscriptValue(params))}`;
|
|
77
80
|
}
|
|
78
81
|
export function formatResponseTranscript(method, message) {
|
|
79
82
|
const error = getErrorObject(message);
|
|
80
83
|
if (error) {
|
|
81
84
|
const code = typeof error.code === "number" ? error.code : "?";
|
|
82
|
-
const messageText =
|
|
83
|
-
?
|
|
84
|
-
:
|
|
85
|
+
const messageText = method === "tasks/list"
|
|
86
|
+
? "[REDACTED]"
|
|
87
|
+
: typeof error.message === "string"
|
|
88
|
+
? sanitizeTranscriptValue(error.message, ["error", "message"])
|
|
89
|
+
: "error";
|
|
85
90
|
return `<- ${method} error ${JSON.stringify({
|
|
86
91
|
code,
|
|
87
92
|
message: messageText
|
|
@@ -129,6 +134,11 @@ export function formatResponseTranscript(method, message) {
|
|
|
129
134
|
prompts: Array.isArray(result.prompts) ? result.prompts.length : 0,
|
|
130
135
|
nextCursor: typeof result.nextCursor === "string" ? "[CURSOR]" : undefined
|
|
131
136
|
})}`;
|
|
137
|
+
case "tasks/list":
|
|
138
|
+
return `<- tasks/list ${JSON.stringify({
|
|
139
|
+
tasks: Array.isArray(result.tasks) ? result.tasks.length : 0,
|
|
140
|
+
nextCursor: typeof result.nextCursor === "string" ? "[CURSOR]" : undefined
|
|
141
|
+
})}`;
|
|
132
142
|
case "prompts/get":
|
|
133
143
|
return `<- prompts/get ${JSON.stringify({
|
|
134
144
|
messages: Array.isArray(result.messages) ? result.messages.length : 0
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -90,6 +90,40 @@ export interface RuntimeScorecard {
|
|
|
90
90
|
resourceTemplatesList: RuntimeCapabilityStatus;
|
|
91
91
|
promptsList: RuntimeCapabilityStatus;
|
|
92
92
|
promptGet: RuntimeCapabilityStatus;
|
|
93
|
+
conformance?: RuntimeConformanceScorecard;
|
|
94
|
+
}
|
|
95
|
+
export type McpConformanceProfile = "legacy" | "2025-11-25" | "future-compatible";
|
|
96
|
+
export interface RuntimeConformanceScorecard {
|
|
97
|
+
protocolVersion: string | null;
|
|
98
|
+
profile: McpConformanceProfile | null;
|
|
99
|
+
capabilityConsistency: RuntimeCapabilityStatus;
|
|
100
|
+
taskDeclarations: RuntimeCapabilityStatus;
|
|
101
|
+
tasksList: RuntimeCapabilityStatus;
|
|
102
|
+
schemaDialect: RuntimeCapabilityStatus;
|
|
103
|
+
overall: "pass" | "warn" | "fail" | "skipped";
|
|
104
|
+
}
|
|
105
|
+
export interface McpToolObservation {
|
|
106
|
+
name: string;
|
|
107
|
+
inputSchema: unknown;
|
|
108
|
+
outputSchema?: unknown;
|
|
109
|
+
execution?: unknown;
|
|
110
|
+
}
|
|
111
|
+
export interface TasksListObservation {
|
|
112
|
+
status: "pass" | "fail" | "skipped";
|
|
113
|
+
itemCount: number;
|
|
114
|
+
pageCount: number;
|
|
115
|
+
failure?: "timeout" | "invalid";
|
|
116
|
+
}
|
|
117
|
+
export interface McpConformanceObservation {
|
|
118
|
+
protocolVersion: string;
|
|
119
|
+
capabilities: unknown;
|
|
120
|
+
tools: McpToolObservation[];
|
|
121
|
+
tasksList?: TasksListObservation;
|
|
122
|
+
skipToolDerivedChecks?: boolean;
|
|
123
|
+
}
|
|
124
|
+
export interface McpConformanceResult {
|
|
125
|
+
findings: Finding[];
|
|
126
|
+
scorecard: RuntimeConformanceScorecard;
|
|
93
127
|
}
|
|
94
128
|
export interface RuntimeProbeResult {
|
|
95
129
|
findings: Finding[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CompatibilityEnvironment, type CompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
|
|
2
|
-
import type { Finding } from "../domain/types.js";
|
|
2
|
+
import type { Finding, RuntimeExecutionEvidence, RuntimeScorecard } from "../domain/types.js";
|
|
3
3
|
import { type SecurityAudit } from "../security/security-audit.js";
|
|
4
4
|
export interface GenericMcpDoctorReport {
|
|
5
5
|
targetPath: string;
|
|
@@ -10,7 +10,13 @@ export interface GenericMcpDoctorReport {
|
|
|
10
10
|
findings: Finding[];
|
|
11
11
|
security: SecurityAudit;
|
|
12
12
|
compatibility: CompatibilityMatrix;
|
|
13
|
+
runtimeScorecard?: RuntimeScorecard;
|
|
14
|
+
runtimeExecution?: RuntimeExecutionEvidence;
|
|
13
15
|
}
|
|
14
|
-
export
|
|
16
|
+
export interface GenericMcpDoctorOptions {
|
|
17
|
+
runtime?: boolean;
|
|
18
|
+
runtimeStartupTimeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildGenericMcpDoctor(targetPath: string, environment?: CompatibilityEnvironment, options?: GenericMcpDoctorOptions): Promise<GenericMcpDoctorReport>;
|
|
15
21
|
export declare function renderGenericMcpDoctorJson(report: GenericMcpDoctorReport): string;
|
|
16
22
|
export declare function renderGenericMcpDoctor(report: GenericMcpDoctorReport): string;
|