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.
Files changed (39) hide show
  1. package/README.md +20 -3
  2. package/dist/core/mcp-prompt-catalog.d.ts +36 -0
  3. package/dist/core/mcp-prompt-catalog.js +53 -0
  4. package/dist/core/mcp-prompt-definition.d.ts +24 -0
  5. package/dist/core/mcp-prompt-definition.js +107 -0
  6. package/dist/core/mcp-prompt-diff.d.ts +43 -0
  7. package/dist/core/mcp-prompt-diff.js +132 -0
  8. package/dist/core/mcp-prompt-file.d.ts +31 -0
  9. package/dist/core/mcp-prompt-file.js +118 -0
  10. package/dist/core/mcp-prompt-impact.d.ts +21 -0
  11. package/dist/core/mcp-prompt-impact.js +93 -0
  12. package/dist/core/mcp-stdio-tool-catalog.d.ts +9 -3
  13. package/dist/core/mcp-stdio-tool-catalog.js +74 -55
  14. package/dist/core/mcp-tool-catalog.d.ts +44 -5
  15. package/dist/core/mcp-tool-catalog.js +76 -31
  16. package/dist/core/mcp-tool-diff.d.ts +8 -2
  17. package/dist/core/mcp-tool-diff.js +7 -5
  18. package/dist/core/mcp-tool-file.d.ts +20 -0
  19. package/dist/core/mcp-tool-file.js +1 -1
  20. package/dist/core/mcp-tool-impact.d.ts +10 -1
  21. package/dist/core/mcp-tool-impact.js +74 -17
  22. package/dist/core/mcp-tool-response-file.d.ts +6 -4
  23. package/dist/core/mcp-tool-response-file.js +8 -8
  24. package/dist/core/output-contract.js +189 -2
  25. package/dist/reporting/quote-server-text.d.ts +3 -0
  26. package/dist/reporting/quote-server-text.js +20 -0
  27. package/dist/reporting/render-mcp-prompt-catalog-report.d.ts +2 -0
  28. package/dist/reporting/render-mcp-prompt-catalog-report.js +31 -0
  29. package/dist/reporting/render-mcp-prompt-diff-markdown.d.ts +2 -0
  30. package/dist/reporting/render-mcp-prompt-diff-markdown.js +39 -0
  31. package/dist/reporting/render-mcp-prompt-diff-report.d.ts +2 -0
  32. package/dist/reporting/render-mcp-prompt-diff-report.js +48 -0
  33. package/dist/reporting/render-mcp-prompt-file-report.d.ts +5 -0
  34. package/dist/reporting/render-mcp-prompt-file-report.js +33 -0
  35. package/dist/reporting/render-mcp-tool-diff-markdown.js +10 -2
  36. package/dist/reporting/render-mcp-tool-diff-report.js +4 -1
  37. package/dist/run-cli.d.ts +7 -0
  38. package/dist/run-cli.js +206 -137
  39. 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 { TOOL_CHANGE_REASONS } from "./mcp-tool-impact.js";
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,3 @@
1
+ /** Quotes server-supplied text, such as a tool name or a JSON Pointer, as a JSON string that only holds printable characters. */
2
+ export declare function quoteServerText(text: string): string;
3
+ export declare function markdownCodeSpan(text: string): string;
@@ -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,2 @@
1
+ import type { McpPromptCatalogReport } from "../core/mcp-prompt-catalog.js";
2
+ export declare function renderMcpPromptCatalogReport(report: McpPromptCatalogReport): string;
@@ -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,2 @@
1
+ import type { McpPromptDiffReport } from "../core/mcp-prompt-diff.js";
2
+ export declare function renderMcpPromptDiffMarkdown(report: McpPromptDiffReport): string;
@@ -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,2 @@
1
+ import type { McpPromptDiffReport } from "../core/mcp-prompt-diff.js";
2
+ export declare function renderMcpPromptDiffReport(report: McpPromptDiffReport): string;
@@ -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("", `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.`);
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
- return `${change.kind.toUpperCase()} [${locations.join(" -> ")}]${change.fields.length ? `: ${change.fields.join(", ")}` : ""}\n Impact: ${impact}`;
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;