codex-plugin-doctor 1.75.0 → 1.77.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 +20 -3
- package/dist/core/mcp-prompt-catalog.d.ts +36 -0
- package/dist/core/mcp-prompt-catalog.js +53 -0
- package/dist/core/mcp-prompt-definition.d.ts +24 -0
- package/dist/core/mcp-prompt-definition.js +107 -0
- package/dist/core/mcp-prompt-diff.d.ts +43 -0
- package/dist/core/mcp-prompt-diff.js +132 -0
- package/dist/core/mcp-prompt-file.d.ts +31 -0
- package/dist/core/mcp-prompt-file.js +118 -0
- package/dist/core/mcp-prompt-impact.d.ts +21 -0
- package/dist/core/mcp-prompt-impact.js +93 -0
- package/dist/core/mcp-stdio-tool-catalog.d.ts +9 -3
- package/dist/core/mcp-stdio-tool-catalog.js +74 -55
- package/dist/core/mcp-tool-catalog.d.ts +44 -5
- package/dist/core/mcp-tool-catalog.js +76 -31
- package/dist/core/mcp-tool-diff.d.ts +8 -2
- package/dist/core/mcp-tool-diff.js +7 -5
- package/dist/core/mcp-tool-file.d.ts +20 -0
- package/dist/core/mcp-tool-file.js +1 -1
- package/dist/core/mcp-tool-impact.d.ts +10 -1
- package/dist/core/mcp-tool-impact.js +74 -17
- package/dist/core/mcp-tool-response-file.d.ts +6 -4
- package/dist/core/mcp-tool-response-file.js +8 -8
- package/dist/core/output-contract.js +189 -2
- package/dist/reporting/quote-server-text.d.ts +3 -0
- package/dist/reporting/quote-server-text.js +20 -0
- package/dist/reporting/render-mcp-prompt-catalog-report.d.ts +2 -0
- package/dist/reporting/render-mcp-prompt-catalog-report.js +31 -0
- package/dist/reporting/render-mcp-prompt-diff-markdown.d.ts +2 -0
- package/dist/reporting/render-mcp-prompt-diff-markdown.js +39 -0
- package/dist/reporting/render-mcp-prompt-diff-report.d.ts +2 -0
- package/dist/reporting/render-mcp-prompt-diff-report.js +48 -0
- package/dist/reporting/render-mcp-prompt-file-report.d.ts +5 -0
- package/dist/reporting/render-mcp-prompt-file-report.js +33 -0
- package/dist/reporting/render-mcp-tool-diff-markdown.js +10 -2
- package/dist/reporting/render-mcp-tool-diff-report.js +4 -1
- package/dist/run-cli.d.ts +7 -0
- package/dist/run-cli.js +206 -137
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { packageVersion } from "../version.js";
|
|
3
|
-
import {
|
|
3
|
+
import { MAX_PROMPT_CHANGE_LOCATIONS, PROMPT_CHANGE_REASONS } from "./mcp-prompt-impact.js";
|
|
4
|
+
import { MAX_TOOL_CHANGE_LOCATIONS, MAX_TOOL_CHANGE_POINTER_LENGTH, TOOL_CHANGE_REASONS } from "./mcp-tool-impact.js";
|
|
4
5
|
import { ruleCatalog } from "../rules/rule-catalog.js";
|
|
5
6
|
const runtimeCapabilityStatusSchema = {
|
|
6
7
|
type: "string",
|
|
@@ -836,7 +837,193 @@ publicSchemaDefinitions.push({
|
|
|
836
837
|
afterToolIndex: { type: ["integer", "null"], minimum: 1, maximum: 500 },
|
|
837
838
|
fields: { type: "array", maxItems: 6, uniqueItems: true, items: { enum: ["inputSchema", "outputSchema", "description", "title", "annotations", "other"] } },
|
|
838
839
|
impact: { enum: ["breaking", "compatible", "unclassified"] },
|
|
839
|
-
reasons: { type: "array", maxItems: TOOL_CHANGE_REASONS.length, uniqueItems: true, items: { enum: [...TOOL_CHANGE_REASONS] } }
|
|
840
|
+
reasons: { type: "array", maxItems: TOOL_CHANGE_REASONS.length, uniqueItems: true, items: { enum: [...TOOL_CHANGE_REASONS] } },
|
|
841
|
+
// Only with --show-names.
|
|
842
|
+
name: { type: "string" },
|
|
843
|
+
locations: {
|
|
844
|
+
type: "array", maxItems: MAX_TOOL_CHANGE_LOCATIONS,
|
|
845
|
+
items: {
|
|
846
|
+
type: "object", required: ["reason", "pointer"],
|
|
847
|
+
properties: { reason: { enum: [...TOOL_CHANGE_REASONS] }, pointer: { type: "string", maxLength: MAX_TOOL_CHANGE_POINTER_LENGTH } },
|
|
848
|
+
additionalProperties: false
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}, additionalProperties: false
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
// The prompt catalog shares discovery, protocol, and execution evidence with the tool catalog.
|
|
857
|
+
const toolCatalogProperties = publicSchemaDefinitions.find((definition) => definition.id === "doctor.tools.json").properties;
|
|
858
|
+
publicSchemaDefinitions.push({
|
|
859
|
+
id: "doctor.prompts.json",
|
|
860
|
+
command: "codex-plugin-doctor doctor prompts <url> --allow-network --json",
|
|
861
|
+
required: ["schemaVersion", "scope", "status", "discovery", "catalog", "coverage", "findings"],
|
|
862
|
+
properties: {
|
|
863
|
+
schemaVersion: { const: 1 },
|
|
864
|
+
scope: { const: "prompt-catalog-structure" },
|
|
865
|
+
// transport, protocol, and execution appear only on reports from `doctor prompts <path> --server <name> --runtime`.
|
|
866
|
+
transport: { const: "stdio" },
|
|
867
|
+
status: toolCatalogProperties.status,
|
|
868
|
+
protocol: toolCatalogProperties.protocol,
|
|
869
|
+
execution: toolCatalogProperties.execution,
|
|
870
|
+
discovery: toolCatalogProperties.discovery,
|
|
871
|
+
catalog: {
|
|
872
|
+
type: "object", required: ["complete", "pagesRead", "promptsChecked", "reason"],
|
|
873
|
+
properties: {
|
|
874
|
+
complete: { type: "boolean" },
|
|
875
|
+
pagesRead: { type: "integer", minimum: 0, maximum: 5 },
|
|
876
|
+
promptsChecked: { type: "integer", minimum: 0, maximum: 500 },
|
|
877
|
+
reason: { type: ["string", "null"] }
|
|
878
|
+
},
|
|
879
|
+
additionalProperties: false
|
|
880
|
+
},
|
|
881
|
+
coverage: {
|
|
882
|
+
type: "object", required: ["discovery", "catalogEnumeration", "definitions", "promptRetrieval", "fullConformance"],
|
|
883
|
+
properties: {
|
|
884
|
+
discovery: { enum: ["pass", "fail", "skipped"] },
|
|
885
|
+
catalogEnumeration: { enum: ["complete", "incomplete", "not-tested"] },
|
|
886
|
+
definitions: { const: "shape-only" },
|
|
887
|
+
promptRetrieval: { const: "not-tested" },
|
|
888
|
+
fullConformance: { const: "not-tested" }
|
|
889
|
+
},
|
|
890
|
+
additionalProperties: false
|
|
891
|
+
},
|
|
892
|
+
findings: {
|
|
893
|
+
type: "array", maxItems: 100,
|
|
894
|
+
items: {
|
|
895
|
+
type: "object", required: ["id", "severity", "message", "impact", "suggestedFix"],
|
|
896
|
+
properties: {
|
|
897
|
+
id: { type: "string", pattern: "^plugin\\.catalog\\." },
|
|
898
|
+
severity: { enum: ["warn", "fail"] },
|
|
899
|
+
message: { type: "string" }, impact: { type: "string" }, suggestedFix: { type: "string" },
|
|
900
|
+
location: {
|
|
901
|
+
type: "object", required: ["page", "promptIndex"],
|
|
902
|
+
properties: {
|
|
903
|
+
page: { type: "integer", minimum: 1, maximum: 5 },
|
|
904
|
+
promptIndex: { type: "integer", minimum: 1, maximum: 500 },
|
|
905
|
+
argumentIndex: { type: "integer", minimum: 1 },
|
|
906
|
+
relatedPromptIndex: { type: "integer", minimum: 1, maximum: 500 },
|
|
907
|
+
relatedArgumentIndex: { type: "integer", minimum: 1 }
|
|
908
|
+
},
|
|
909
|
+
additionalProperties: false
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
additionalProperties: false
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
const promptFileReportDefinition = {
|
|
918
|
+
id: "doctor.prompts.file.json",
|
|
919
|
+
command: "codex-plugin-doctor doctor prompts-file <path> --json",
|
|
920
|
+
required: ["schemaVersion", "scope", "status", "source", "inspection", "coverage", "findings"],
|
|
921
|
+
properties: {
|
|
922
|
+
schemaVersion: { const: 1 },
|
|
923
|
+
scope: { const: "prompt-definitions-file" },
|
|
924
|
+
status: { enum: ["pass", "warn", "fail", "incomplete", "blocked"] },
|
|
925
|
+
source: {
|
|
926
|
+
type: "object", required: ["kind", "format", "bytesRead"],
|
|
927
|
+
properties: {
|
|
928
|
+
kind: { const: "file" }, format: { const: "mcp-prompts-list-response" },
|
|
929
|
+
bytesRead: { type: "integer", minimum: 0, maximum: 1048577 }
|
|
930
|
+
}, additionalProperties: false
|
|
931
|
+
},
|
|
932
|
+
inspection: {
|
|
933
|
+
type: "object", required: ["complete", "promptsChecked", "hasNextCursor", "reason"],
|
|
934
|
+
properties: {
|
|
935
|
+
complete: { type: "boolean" }, promptsChecked: { type: "integer", minimum: 0, maximum: 500 },
|
|
936
|
+
hasNextCursor: { type: ["boolean", "null"] }, reason: { type: ["string", "null"] }
|
|
937
|
+
}, additionalProperties: false
|
|
938
|
+
},
|
|
939
|
+
coverage: {
|
|
940
|
+
type: "object", required: ["definitions", "discovery", "serverCatalog", "promptRetrieval", "fullConformance"],
|
|
941
|
+
properties: {
|
|
942
|
+
definitions: { const: "shape-only" }, discovery: { const: "not-tested" },
|
|
943
|
+
serverCatalog: { const: "not-tested" }, promptRetrieval: { const: "not-tested" },
|
|
944
|
+
fullConformance: { const: "not-tested" }
|
|
945
|
+
}, additionalProperties: false
|
|
946
|
+
},
|
|
947
|
+
findings: {
|
|
948
|
+
type: "array", maxItems: 100,
|
|
949
|
+
items: {
|
|
950
|
+
type: "object", required: ["id", "severity", "message", "impact", "suggestedFix"],
|
|
951
|
+
properties: {
|
|
952
|
+
id: { type: "string", pattern: "^plugin\\.(catalog|prompts_file)\\." },
|
|
953
|
+
severity: { enum: ["warn", "fail"] },
|
|
954
|
+
message: { type: "string" }, impact: { type: "string" }, suggestedFix: { type: "string" },
|
|
955
|
+
location: {
|
|
956
|
+
type: "object", required: ["page", "promptIndex"],
|
|
957
|
+
properties: {
|
|
958
|
+
page: { const: 1 }, promptIndex: { type: "integer", minimum: 1, maximum: 500 },
|
|
959
|
+
argumentIndex: { type: "integer", minimum: 1 },
|
|
960
|
+
relatedPromptIndex: { type: "integer", minimum: 1, maximum: 500 },
|
|
961
|
+
relatedArgumentIndex: { type: "integer", minimum: 1 }
|
|
962
|
+
}, additionalProperties: false
|
|
963
|
+
}
|
|
964
|
+
}, additionalProperties: false
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
const embeddedPromptFileReportSchema = {
|
|
970
|
+
type: "object",
|
|
971
|
+
required: promptFileReportDefinition.required,
|
|
972
|
+
properties: promptFileReportDefinition.properties,
|
|
973
|
+
additionalProperties: true
|
|
974
|
+
};
|
|
975
|
+
publicSchemaDefinitions.push(promptFileReportDefinition, {
|
|
976
|
+
id: "doctor.prompts.diff.json",
|
|
977
|
+
command: "codex-plugin-doctor doctor prompts-diff --before <path> --after <path> --json",
|
|
978
|
+
required: ["schemaVersion", "scope", "status", "before", "after", "comparison", "coverage", "changes"],
|
|
979
|
+
properties: {
|
|
980
|
+
schemaVersion: { const: 1 },
|
|
981
|
+
scope: { const: "prompt-definitions-diff" },
|
|
982
|
+
status: { enum: ["pass", "warn", "incomplete", "blocked"] },
|
|
983
|
+
before: embeddedPromptFileReportSchema,
|
|
984
|
+
after: embeddedPromptFileReportSchema,
|
|
985
|
+
comparison: {
|
|
986
|
+
type: "object", required: ["complete", "reason", "added", "removed", "changed", "unchanged", "breaking", "unclassified"],
|
|
987
|
+
properties: {
|
|
988
|
+
complete: { type: "boolean" },
|
|
989
|
+
reason: { enum: [null, "input-blocked", "input-incomplete", "invalid-definitions", "ambiguous-names"] },
|
|
990
|
+
added: { type: ["integer", "null"], minimum: 0, maximum: 500 },
|
|
991
|
+
removed: { type: ["integer", "null"], minimum: 0, maximum: 500 },
|
|
992
|
+
changed: { type: ["integer", "null"], minimum: 0, maximum: 500 },
|
|
993
|
+
unchanged: { type: ["integer", "null"], minimum: 0, maximum: 500 },
|
|
994
|
+
breaking: { type: ["integer", "null"], minimum: 0, maximum: 1000 },
|
|
995
|
+
unclassified: { type: ["integer", "null"], minimum: 0, maximum: 1000 }
|
|
996
|
+
}, additionalProperties: false
|
|
997
|
+
},
|
|
998
|
+
coverage: {
|
|
999
|
+
type: "object", required: ["comparison", "definitions", "compatibility", "serverCatalog", "promptRetrieval", "impactClassification"],
|
|
1000
|
+
properties: {
|
|
1001
|
+
comparison: { const: "structural-only" }, definitions: { const: "shape-only" },
|
|
1002
|
+
compatibility: { const: "not-tested" }, serverCatalog: { const: "not-tested" },
|
|
1003
|
+
promptRetrieval: { const: "not-tested" }, impactClassification: { const: "heuristic" }
|
|
1004
|
+
}, additionalProperties: false
|
|
1005
|
+
},
|
|
1006
|
+
changes: {
|
|
1007
|
+
type: "array", maxItems: 1000,
|
|
1008
|
+
items: {
|
|
1009
|
+
type: "object", required: ["kind", "beforePromptIndex", "afterPromptIndex", "fields", "impact", "reasons"],
|
|
1010
|
+
properties: {
|
|
1011
|
+
kind: { enum: ["added", "removed", "changed"] },
|
|
1012
|
+
beforePromptIndex: { type: ["integer", "null"], minimum: 1, maximum: 500 },
|
|
1013
|
+
afterPromptIndex: { type: ["integer", "null"], minimum: 1, maximum: 500 },
|
|
1014
|
+
fields: { type: "array", maxItems: 4, uniqueItems: true, items: { enum: ["arguments", "description", "title", "other"] } },
|
|
1015
|
+
impact: { enum: ["breaking", "compatible", "unclassified"] },
|
|
1016
|
+
reasons: { type: "array", maxItems: PROMPT_CHANGE_REASONS.length, uniqueItems: true, items: { enum: [...PROMPT_CHANGE_REASONS] } },
|
|
1017
|
+
// Only with --show-names.
|
|
1018
|
+
name: { type: "string" },
|
|
1019
|
+
locations: {
|
|
1020
|
+
type: "array", maxItems: MAX_PROMPT_CHANGE_LOCATIONS,
|
|
1021
|
+
items: {
|
|
1022
|
+
type: "object", required: ["reason", "argument"],
|
|
1023
|
+
properties: { reason: { enum: [...PROMPT_CHANGE_REASONS] }, argument: { type: ["string", "null"] } },
|
|
1024
|
+
additionalProperties: false
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
840
1027
|
}, additionalProperties: false
|
|
841
1028
|
}
|
|
842
1029
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Control, format, and line or paragraph separator characters, which can drive a terminal, end a line,
|
|
2
|
+
// reorder text, or hide in it; JSON escapes only the C0 controls among them.
|
|
3
|
+
const UNSAFE_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
4
|
+
/** Quotes server-supplied text, such as a tool name or a JSON Pointer, as a JSON string that only holds printable characters. */
|
|
5
|
+
export function quoteServerText(text) {
|
|
6
|
+
return JSON.stringify(text).replace(UNSAFE_TEXT, (character) => Array.from({ length: character.length }, (_, index) => `\\u${character.charCodeAt(index).toString(16).padStart(4, "0")}`).join(""));
|
|
7
|
+
}
|
|
8
|
+
// Names and pointers come from the server, so they are quoted and put in a code span, where Markdown,
|
|
9
|
+
// HTML, mentions, and links stay inert; the fence outgrows any backtick run, and pipes are escaped for the table.
|
|
10
|
+
export function markdownCodeSpan(text) {
|
|
11
|
+
const quoted = quoteServerText(text);
|
|
12
|
+
let run = 0;
|
|
13
|
+
let longest = 0;
|
|
14
|
+
for (const character of quoted) {
|
|
15
|
+
run = character === "`" ? run + 1 : 0;
|
|
16
|
+
longest = Math.max(longest, run);
|
|
17
|
+
}
|
|
18
|
+
const fence = "`".repeat(longest + 1);
|
|
19
|
+
return `${fence}${quoted.replace(/\|/gu, "\\|")}${fence}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describePromptLocation } from "./render-mcp-prompt-file-report.js";
|
|
2
|
+
export function renderMcpPromptCatalogReport(report) {
|
|
3
|
+
return [
|
|
4
|
+
"MCP Prompt Catalog",
|
|
5
|
+
"==================",
|
|
6
|
+
`Status: ${report.status}`,
|
|
7
|
+
`Scope: ${report.scope}`,
|
|
8
|
+
...(report.transport === "stdio"
|
|
9
|
+
? [
|
|
10
|
+
"Transport: stdio",
|
|
11
|
+
`Protocol: ${report.protocol?.era ?? "not established"}${report.protocol?.version ? ` (${report.protocol.version})` : ""}`,
|
|
12
|
+
`Execution: ${report.execution?.backend ?? "not started"}`
|
|
13
|
+
]
|
|
14
|
+
: []),
|
|
15
|
+
`Discovery: ${report.discovery.status}`,
|
|
16
|
+
`Catalog complete: ${report.catalog.complete}`,
|
|
17
|
+
`Pages read: ${report.catalog.pagesRead}`,
|
|
18
|
+
`Prompts checked: ${report.catalog.promptsChecked}`,
|
|
19
|
+
...(report.catalog.reason === null ? [] : [`Reason: ${report.catalog.reason}`]),
|
|
20
|
+
`Enumeration: ${report.coverage.catalogEnumeration}`,
|
|
21
|
+
`Definition coverage: ${report.coverage.definitions}`,
|
|
22
|
+
`Prompt retrieval: ${report.coverage.promptRetrieval}`,
|
|
23
|
+
`Full conformance: ${report.coverage.fullConformance}`,
|
|
24
|
+
"Counts describe inspected content, not a guaranteed server-wide inventory.",
|
|
25
|
+
...report.discovery.findings.map((finding) => `${finding.severity.toUpperCase()} ${finding.id}: ${finding.message}\n ${finding.suggestedFix}`),
|
|
26
|
+
...report.findings.map((finding) => {
|
|
27
|
+
const locator = finding.location ? ` [page ${finding.location.page}, ${describePromptLocation(finding.location)}]` : "";
|
|
28
|
+
return `${finding.severity.toUpperCase()} ${finding.id}${locator}: ${finding.message}\n ${finding.suggestedFix}`;
|
|
29
|
+
})
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { markdownCodeSpan } from "./quote-server-text.js";
|
|
2
|
+
import { describePromptLocation } from "./render-mcp-prompt-file-report.js";
|
|
3
|
+
function count(value) {
|
|
4
|
+
return value === null || value === undefined ? "not compared" : String(value);
|
|
5
|
+
}
|
|
6
|
+
function promptIndex(value) {
|
|
7
|
+
return value === null ? "-" : String(value);
|
|
8
|
+
}
|
|
9
|
+
export function renderMcpPromptDiffMarkdown(report) {
|
|
10
|
+
const comparison = report.comparison;
|
|
11
|
+
const named = report.changes.some((change) => change.name !== undefined);
|
|
12
|
+
const lines = [
|
|
13
|
+
"## MCP Prompt Definition Diff",
|
|
14
|
+
"",
|
|
15
|
+
`**Status:** \`${report.status}\` · **Comparison complete:** ${comparison.complete ? "yes" : "no"}${comparison.reason === null ? "" : ` · **Reason:** \`${comparison.reason}\``}`,
|
|
16
|
+
"",
|
|
17
|
+
"| Added | Removed | Changed | Unchanged | Breaking | Unclassified |",
|
|
18
|
+
"| --- | --- | --- | --- | --- | --- |",
|
|
19
|
+
`| ${count(comparison.added)} | ${count(comparison.removed)} | ${count(comparison.changed)} | ${count(comparison.unchanged)} | ${count(comparison.breaking)} | ${count(comparison.unclassified)} |`
|
|
20
|
+
];
|
|
21
|
+
if (report.changes.length > 0) {
|
|
22
|
+
lines.push("", "### Changes", "", named ? "| Change | Prompt | Before prompt | After prompt | Fields | Impact | Reasons |" : "| Change | Before prompt | After prompt | Fields | Impact | Reasons |", named ? "| --- | --- | --- | --- | --- | --- | --- |" : "| --- | --- | --- | --- | --- | --- |", ...report.changes.map((change) => `| ${change.kind}${named ? ` | ${change.name === undefined ? "-" : markdownCodeSpan(change.name)}` : ""} | ${promptIndex(change.beforePromptIndex)} | ${promptIndex(change.afterPromptIndex)} | ${change.fields.map((field) => `\`${field}\``).join(", ") || "-"} | **${change.impact}** | ${change.reasons.map((reason) => `\`${reason}\``).join(", ") || "-"} |`));
|
|
23
|
+
}
|
|
24
|
+
const located = report.changes.flatMap((change) => (change.locations ?? []).map((location) => `| ${change.name === undefined ? "-" : markdownCodeSpan(change.name)} | \`${location.reason}\` | ${location.argument === null ? "-" : markdownCodeSpan(location.argument)} |`));
|
|
25
|
+
if (located.length > 0) {
|
|
26
|
+
lines.push("", "### Locations", "", "| Prompt | Reason | Argument |", "| --- | --- | --- |", ...located);
|
|
27
|
+
}
|
|
28
|
+
const findings = ["before", "after"].flatMap((side) => report[side].findings.map((finding) => {
|
|
29
|
+
const locator = finding.location ? `${side} ${describePromptLocation(finding.location)}` : side;
|
|
30
|
+
return `| ${finding.severity} | \`${finding.id}\` | ${locator} | ${finding.message} |`;
|
|
31
|
+
}));
|
|
32
|
+
if (findings.length > 0) {
|
|
33
|
+
lines.push("", "### Input Findings", "", "| Severity | Finding | Location | Message |", "| --- | --- | --- | --- |", ...findings);
|
|
34
|
+
}
|
|
35
|
+
lines.push("", named
|
|
36
|
+
? `Prompt and argument names are shown because --show-names was used; descriptions and other values are not reported. Impact labels are ${report.coverage.impactClassification} argument checks, not proof of compatibility.`
|
|
37
|
+
: `Prompt numbers are one-based positions in the before and after files; names and descriptions are not reported. Impact labels are ${report.coverage.impactClassification} argument checks, not proof of compatibility.`);
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describePromptLocation } from "./render-mcp-prompt-file-report.js";
|
|
2
|
+
import { quoteServerText } from "./quote-server-text.js";
|
|
3
|
+
export function renderMcpPromptDiffReport(report) {
|
|
4
|
+
const comparison = report.comparison;
|
|
5
|
+
return [
|
|
6
|
+
"Offline MCP Prompt Diff",
|
|
7
|
+
"=======================",
|
|
8
|
+
`Status: ${report.status}`,
|
|
9
|
+
`Scope: ${report.scope}`,
|
|
10
|
+
`Comparison complete: ${comparison.complete}`,
|
|
11
|
+
...(comparison.reason === null ? [] : [`Reason: ${comparison.reason}`]),
|
|
12
|
+
`Added: ${comparison.added ?? "not compared"}`,
|
|
13
|
+
`Removed: ${comparison.removed ?? "not compared"}`,
|
|
14
|
+
`Changed: ${comparison.changed ?? "not compared"}`,
|
|
15
|
+
`Unchanged: ${comparison.unchanged ?? "not compared"}`,
|
|
16
|
+
`Breaking: ${comparison.breaking ?? "not compared"}`,
|
|
17
|
+
`Unclassified: ${comparison.unclassified ?? "not compared"}`,
|
|
18
|
+
`Comparison coverage: ${report.coverage.comparison}`,
|
|
19
|
+
`Definition coverage: ${report.coverage.definitions}`,
|
|
20
|
+
`Compatibility: ${report.coverage.compatibility}`,
|
|
21
|
+
`Server catalog: ${report.coverage.serverCatalog}`,
|
|
22
|
+
`Prompt retrieval: ${report.coverage.promptRetrieval}`,
|
|
23
|
+
`Impact classification: ${report.coverage.impactClassification}`,
|
|
24
|
+
"Only the supplied responses are compared. Impact labels are argument heuristics: a compatible label or an unchanged result does not establish compatibility.",
|
|
25
|
+
...["before", "after"].flatMap((side) => {
|
|
26
|
+
const source = report[side];
|
|
27
|
+
const label = side === "before" ? "Before" : "After";
|
|
28
|
+
return [
|
|
29
|
+
`${label} input: ${source.status}; prompts checked: ${source.inspection.promptsChecked}; bytes read: ${source.source.bytesRead}`,
|
|
30
|
+
...(source.inspection.reason === null ? [] : [`${label} reason: ${source.inspection.reason}`]),
|
|
31
|
+
...source.findings.map((finding) => {
|
|
32
|
+
const locator = finding.location ? ` [${side} ${describePromptLocation(finding.location)}]` : "";
|
|
33
|
+
return `${label} ${finding.severity.toUpperCase()} ${finding.id}${locator}: ${finding.message}\n ${finding.suggestedFix}`;
|
|
34
|
+
})
|
|
35
|
+
];
|
|
36
|
+
}),
|
|
37
|
+
...report.changes.map((change) => {
|
|
38
|
+
const locations = [
|
|
39
|
+
...(change.beforePromptIndex === null ? [] : [`before prompt ${change.beforePromptIndex}`]),
|
|
40
|
+
...(change.afterPromptIndex === null ? [] : [`after prompt ${change.afterPromptIndex}`])
|
|
41
|
+
];
|
|
42
|
+
const impact = `${change.impact}${change.reasons.length ? ` (${change.reasons.join(", ")})` : ""}`;
|
|
43
|
+
const name = change.name === undefined ? "" : ` ${quoteServerText(change.name)}`;
|
|
44
|
+
const at = (change.locations ?? []).map((location) => `\n At ${location.argument === null ? "(prompt)" : `argument ${quoteServerText(location.argument)}`}: ${location.reason}`).join("");
|
|
45
|
+
return `${change.kind.toUpperCase()}${name} [${locations.join(" -> ")}]${change.fields.length ? `: ${change.fields.join(", ")}` : ""}\n Impact: ${impact}${at}`;
|
|
46
|
+
})
|
|
47
|
+
].join("\n");
|
|
48
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PromptCatalogLocation } from "../core/mcp-prompt-definition.js";
|
|
2
|
+
import type { McpPromptFileReport } from "../core/mcp-prompt-file.js";
|
|
3
|
+
/** Describes a finding location by one-based positions only, such as "prompt 2, argument 1, related argument 3". */
|
|
4
|
+
export declare function describePromptLocation(location: PromptCatalogLocation): string;
|
|
5
|
+
export declare function renderMcpPromptFileReport(report: McpPromptFileReport): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Describes a finding location by one-based positions only, such as "prompt 2, argument 1, related argument 3". */
|
|
2
|
+
export function describePromptLocation(location) {
|
|
3
|
+
return [
|
|
4
|
+
`prompt ${location.promptIndex}`,
|
|
5
|
+
...(location.argumentIndex === undefined ? [] : [`argument ${location.argumentIndex}`]),
|
|
6
|
+
...(location.relatedPromptIndex === undefined ? [] : [`related prompt ${location.relatedPromptIndex}`]),
|
|
7
|
+
...(location.relatedArgumentIndex === undefined ? [] : [`related argument ${location.relatedArgumentIndex}`])
|
|
8
|
+
].join(", ");
|
|
9
|
+
}
|
|
10
|
+
export function renderMcpPromptFileReport(report) {
|
|
11
|
+
return [
|
|
12
|
+
"Offline MCP Prompt Definitions",
|
|
13
|
+
"==============================",
|
|
14
|
+
`Status: ${report.status}`,
|
|
15
|
+
`Scope: ${report.scope}`,
|
|
16
|
+
`Source: ${report.source.kind} (${report.source.format})`,
|
|
17
|
+
`Bytes read: ${report.source.bytesRead}`,
|
|
18
|
+
`Inspection complete: ${report.inspection.complete}`,
|
|
19
|
+
`Prompts checked: ${report.inspection.promptsChecked}`,
|
|
20
|
+
`More pages advertised: ${report.inspection.hasNextCursor ?? "unknown"}`,
|
|
21
|
+
...(report.inspection.reason === null ? [] : [`Reason: ${report.inspection.reason}`]),
|
|
22
|
+
`Definition coverage: ${report.coverage.definitions}`,
|
|
23
|
+
`Discovery: ${report.coverage.discovery}`,
|
|
24
|
+
`Server catalog: ${report.coverage.serverCatalog}`,
|
|
25
|
+
`Prompt retrieval: ${report.coverage.promptRetrieval}`,
|
|
26
|
+
`Full conformance: ${report.coverage.fullConformance}`,
|
|
27
|
+
"Only the supplied file was inspected. Its provenance and server completeness are not established.",
|
|
28
|
+
...report.findings.map((finding) => {
|
|
29
|
+
const locator = finding.location ? ` [${describePromptLocation(finding.location)}]` : "";
|
|
30
|
+
return `${finding.severity.toUpperCase()} ${finding.id}${locator}: ${finding.message}\n ${finding.suggestedFix}`;
|
|
31
|
+
})
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { markdownCodeSpan } from "./quote-server-text.js";
|
|
1
2
|
function count(value) {
|
|
2
3
|
return value === null || value === undefined ? "not compared" : String(value);
|
|
3
4
|
}
|
|
@@ -6,6 +7,7 @@ function toolIndex(value) {
|
|
|
6
7
|
}
|
|
7
8
|
export function renderMcpToolDiffMarkdown(report) {
|
|
8
9
|
const comparison = report.comparison;
|
|
10
|
+
const named = report.changes.some((change) => change.name !== undefined);
|
|
9
11
|
const lines = [
|
|
10
12
|
"## MCP Tool Definition Diff",
|
|
11
13
|
"",
|
|
@@ -16,7 +18,11 @@ export function renderMcpToolDiffMarkdown(report) {
|
|
|
16
18
|
`| ${count(comparison.added)} | ${count(comparison.removed)} | ${count(comparison.changed)} | ${count(comparison.unchanged)} | ${count(comparison.breaking)} | ${count(comparison.unclassified)} |`
|
|
17
19
|
];
|
|
18
20
|
if (report.changes.length > 0) {
|
|
19
|
-
lines.push("", "### Changes", "", "| Change | Before tool | After tool | Fields | Impact | Reasons |", "| --- | --- | --- | --- | --- | --- |", ...report.changes.map((change) => `| ${change.kind} | ${toolIndex(change.beforeToolIndex)} | ${toolIndex(change.afterToolIndex)} | ${change.fields.map((field) => `\`${field}\``).join(", ") || "-"} | **${change.impact}** | ${change.reasons.map((reason) => `\`${reason}\``).join(", ") || "-"} |`));
|
|
21
|
+
lines.push("", "### Changes", "", named ? "| Change | Tool | Before tool | After tool | Fields | Impact | Reasons |" : "| Change | Before tool | After tool | Fields | Impact | Reasons |", named ? "| --- | --- | --- | --- | --- | --- | --- |" : "| --- | --- | --- | --- | --- | --- |", ...report.changes.map((change) => `| ${change.kind}${named ? ` | ${change.name === undefined ? "-" : markdownCodeSpan(change.name)}` : ""} | ${toolIndex(change.beforeToolIndex)} | ${toolIndex(change.afterToolIndex)} | ${change.fields.map((field) => `\`${field}\``).join(", ") || "-"} | **${change.impact}** | ${change.reasons.map((reason) => `\`${reason}\``).join(", ") || "-"} |`));
|
|
22
|
+
}
|
|
23
|
+
const located = report.changes.flatMap((change) => (change.locations ?? []).map((location) => `| ${change.name === undefined ? "-" : markdownCodeSpan(change.name)} | \`${location.reason}\` | ${location.pointer === "" ? "(tool)" : markdownCodeSpan(location.pointer)} |`));
|
|
24
|
+
if (located.length > 0) {
|
|
25
|
+
lines.push("", "### Locations", "", "| Tool | Reason | Location |", "| --- | --- | --- |", ...located);
|
|
20
26
|
}
|
|
21
27
|
const findings = ["before", "after"].flatMap((side) => report[side].findings.map((finding) => {
|
|
22
28
|
const location = finding.location;
|
|
@@ -28,6 +34,8 @@ export function renderMcpToolDiffMarkdown(report) {
|
|
|
28
34
|
if (findings.length > 0) {
|
|
29
35
|
lines.push("", "### Input Findings", "", "| Severity | Finding | Location | Message |", "| --- | --- | --- | --- |", ...findings);
|
|
30
36
|
}
|
|
31
|
-
lines.push("",
|
|
37
|
+
lines.push("", named
|
|
38
|
+
? `Tool names and schema locations are shown because --show-names was used; schema values are not reported. Impact labels are ${report.coverage.impactClassification} schema checks, not proof of compatibility.`
|
|
39
|
+
: `Tool numbers are one-based positions in the before and after files; names and schema values are not reported. Impact labels are ${report.coverage.impactClassification} schema checks, not proof of compatibility.`);
|
|
32
40
|
return lines.join("\n");
|
|
33
41
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { quoteServerText } from "./quote-server-text.js";
|
|
1
2
|
export function renderMcpToolDiffReport(report) {
|
|
2
3
|
const comparison = report.comparison;
|
|
3
4
|
return [
|
|
@@ -39,7 +40,9 @@ export function renderMcpToolDiffReport(report) {
|
|
|
39
40
|
...(change.afterToolIndex === null ? [] : [`after tool ${change.afterToolIndex}`])
|
|
40
41
|
];
|
|
41
42
|
const impact = `${change.impact}${change.reasons.length ? ` (${change.reasons.join(", ")})` : ""}`;
|
|
42
|
-
|
|
43
|
+
const name = change.name === undefined ? "" : ` ${quoteServerText(change.name)}`;
|
|
44
|
+
const at = (change.locations ?? []).map((location) => `\n At ${location.pointer === "" ? "(tool)" : quoteServerText(location.pointer)}: ${location.reason}`).join("");
|
|
45
|
+
return `${change.kind.toUpperCase()}${name} [${locations.join(" -> ")}]${change.fields.length ? `: ${change.fields.join(", ")}` : ""}\n Impact: ${impact}${at}`;
|
|
43
46
|
})
|
|
44
47
|
].join("\n");
|
|
45
48
|
}
|
package/dist/run-cli.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { discoverMcpServer } from "./core/mcp-discovery.js";
|
|
2
2
|
import { captureMcpToolCatalog, inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
|
|
3
3
|
import { captureStdioMcpToolCatalog } from "./core/mcp-stdio-tool-catalog.js";
|
|
4
|
+
import { captureMcpPromptCatalog, captureStdioMcpPromptCatalog } from "./core/mcp-prompt-catalog.js";
|
|
5
|
+
import { compareMcpPromptFiles } from "./core/mcp-prompt-diff.js";
|
|
6
|
+
import { inspectMcpPromptFile } from "./core/mcp-prompt-file.js";
|
|
4
7
|
import { inspectMcpToolFile } from "./core/mcp-tool-file.js";
|
|
5
8
|
import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
|
|
6
9
|
import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport } from "./core/corpus-quality-metrics.js";
|
|
@@ -23,6 +26,10 @@ export interface RunCliOptions {
|
|
|
23
26
|
inspectMcpToolCatalogImpl?: typeof inspectMcpToolCatalog;
|
|
24
27
|
captureMcpToolCatalogImpl?: typeof captureMcpToolCatalog;
|
|
25
28
|
captureStdioMcpToolCatalogImpl?: typeof captureStdioMcpToolCatalog;
|
|
29
|
+
compareMcpPromptFilesImpl?: typeof compareMcpPromptFiles;
|
|
30
|
+
inspectMcpPromptFileImpl?: typeof inspectMcpPromptFile;
|
|
31
|
+
captureMcpPromptCatalogImpl?: typeof captureMcpPromptCatalog;
|
|
32
|
+
captureStdioMcpPromptCatalogImpl?: typeof captureStdioMcpPromptCatalog;
|
|
26
33
|
discoverMcpServerImpl?: typeof discoverMcpServer;
|
|
27
34
|
terminalContext?: CliTerminalContext;
|
|
28
35
|
runCheckImpl?: typeof runCheck;
|