mcp-medic 1.2.2 → 1.2.4

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 (38) hide show
  1. package/README.md +82 -8
  2. package/dist/checks/index.d.ts +8 -0
  3. package/dist/checks/index.js +24 -0
  4. package/dist/checks/malformed-schema.js +17 -0
  5. package/dist/checks/protocol-connection-health.d.ts +17 -0
  6. package/dist/checks/protocol-connection-health.js +78 -0
  7. package/dist/checks/quality-prompts.d.ts +8 -0
  8. package/dist/checks/quality-prompts.js +121 -0
  9. package/dist/checks/quality-resources.d.ts +8 -0
  10. package/dist/checks/quality-resources.js +92 -0
  11. package/dist/checks/quality-tool-annotations.d.ts +10 -0
  12. package/dist/checks/quality-tool-annotations.js +63 -0
  13. package/dist/checks/quality-tool-descriptions.d.ts +2 -0
  14. package/dist/checks/quality-tool-descriptions.js +106 -0
  15. package/dist/checks/quality-tool-names.d.ts +35 -0
  16. package/dist/checks/quality-tool-names.js +160 -0
  17. package/dist/checks/quality-tool-output-schema.d.ts +10 -0
  18. package/dist/checks/quality-tool-output-schema.js +76 -0
  19. package/dist/checks/quality-tool-surface.d.ts +13 -0
  20. package/dist/checks/quality-tool-surface.js +99 -0
  21. package/dist/cli.d.ts +3 -1
  22. package/dist/cli.js +35 -3
  23. package/dist/diagnostics.d.ts +11 -0
  24. package/dist/diagnostics.js +28 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +5 -0
  27. package/dist/orchestrator.js +7 -1
  28. package/dist/policy.d.ts +9 -0
  29. package/dist/policy.js +72 -0
  30. package/dist/protocol/connect.js +42 -1
  31. package/dist/protocol/quality-rules.d.ts +46 -0
  32. package/dist/protocol/quality-rules.js +37 -0
  33. package/dist/quality-score.d.ts +76 -0
  34. package/dist/quality-score.js +242 -0
  35. package/dist/report.d.ts +3 -0
  36. package/dist/report.js +54 -0
  37. package/dist/types.d.ts +81 -1
  38. package/package.json +1 -1
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
  [![npm version](https://img.shields.io/npm/v/mcp-medic.svg)](https://www.npmjs.com/package/mcp-medic)
5
5
  [![license](https://img.shields.io/npm/l/mcp-medic.svg)](./LICENSE)
6
6
 
7
- Diagnose broken MCP (Model Context Protocol) server configs before they break your agent silently.
7
+ An MCP quality gate for developers and CI — not just "does my config parse," but "is this MCP server high-quality, safe, well-described, and usable by an AI agent."
8
8
 
9
- `mcp-medic` validates MCP server configurations, executes full protocol initialization handshakes across stdio/SSE/HTTP transports, checks all exposed tool JSON schemas against standard specifications, and simulates sample calls — providing actionable suggestions and CI-ready exit codes.
9
+ `mcp-medic` validates MCP server configurations, executes full protocol initialization handshakes across stdio/SSE/HTTP transports (with real protocol version negotiation), passively inspects every declared tool/resource/prompt, checks JSON schemas against the MCP spec, and computes a deterministic 0–100 **MCP Quality Score** — providing actionable diagnostics, a stable diagnostic taxonomy, and CI-ready exit codes.
10
10
 
11
11
  > [!NOTE]
12
12
  > **Naming & Installation**: The npm package for this tool is **`mcp-medic`** (`npx mcp-medic` / `npm i -g mcp-medic`). While this GitHub repository is named `mcp-doctor`, an unrelated older package already occupies the npm name `mcp-doctor` (different author). Users who want this tool must install **`mcp-medic`**, not `mcp-doctor`.
@@ -27,16 +27,18 @@ This is the whole point (a real handshake, not a schema guess) — but it means
27
27
 
28
28
  ## Features
29
29
 
30
+ - 🏆 **MCP Quality Score**: a deterministic 0–100 score across five weighted dimensions (Protocol, Schema, Agent usability, Security, Reliability) via `mcp-medic score <config>` or `check --score`. No LLM, no randomness — every point is traced back to a real diagnostic.
31
+ - 🧠 **Tool/Resource/Prompt Quality Checks**: flags empty/duplicate/placeholder tool names, vague or placeholder descriptions, malformed input/output schemas, contradictory tool annotations, excessive tool surface (bloat), and duplicate/malformed resources and prompts.
30
32
  - 🔍 **Auto-Discovery**: Run `mcp-medic check` with no arguments to auto-discover Claude Desktop, `.mcp.json`, and VS Code/Cursor MCP configuration paths across macOS, Windows, and Linux.
31
33
  - 💡 **Auto-Fix Suggestions**: Diagnose issues with clear, actionable fix suggestions using `--show-fixes`.
32
34
  - 🌐 **Registry Validation**: Validate published registry entries directly using `mcp-medic check --registry <server-id>`.
33
35
  - 🧪 **Fleet Validation** (experimental): Scan and validate monorepos or multi-team configurations with `mcp-medic check-all "<glob>"`.
34
36
  - 🧪 **Drift Detection** (experimental): Catch environment divergence between staging and production configs with `mcp-medic diff <configA> <configB>`.
35
- - 📜 **Policy-as-Code**: Enforce organizational constraints (e.g., banned transports, domain allowlists, minimum description lengths) via `.mcp-medic-policy.json` / `--policy`.
37
+ - 📜 **Policy-as-Code**: Enforce organizational constraints (banned transports, domain allowlists, minimum description lengths, a minimum quality score, a max tool count, required tool descriptions) via `.mcp-medic-policy.json` / `--policy`.
36
38
  - 📸 **Snapshot Baseline Mode**: Filter out legacy diagnostics with `--snapshot <baseline.json>` to gate only on newly introduced regressions.
37
- - 📊 **CI Reporting**: Export standard JUnit XML (`--export-junit <file.xml>`) and JSON (`--export-json <file.json>`) for seamless CI dashboard visualization.
39
+ - 📊 **CI Reporting**: Export JUnit XML (`--export-junit`), JSON (`--export-json`, always includes the quality score), and SARIF 2.1.0 (`--export-sarif`, for GitHub Code Scanning).
38
40
  - 👀 **Watch Mode**: Re-run validation on save using `mcp-medic watch <path>`.
39
- - ⚡ **Transport Hardening**: Full handshake validation across stdio, HTTP (with OAuth token refresh), and SSE (with automatic retry resilience).
41
+ - ⚡ **Protocol Correctness**: real protocol version negotiation (client requests a version, server's actual negotiated version is validated — not assumed), full handshake validation across stdio, HTTP (with OAuth token refresh), and SSE (with automatic retry resilience), plus passive `resources/list`/`prompts/list` capability inspection.
40
42
  - 🧪 **VS Code Extension** (experimental, not yet on the Marketplace): in-editor squiggles and hover tooltips — runnable from source today, see [vscode-extension/](./vscode-extension/).
41
43
  - 🚦 **CI Usability & Exit Codes**: Strict exit code taxonomy (`0` clean, `1` diagnostic failures, `2` usage/syntax errors) and `--fail-on <error|warning>`.
42
44
  - 🤖 **GitHub Action**: Drop-in CI integration via `shivam039/mcp-doctor@main` (or `mcp-medic-action`).
@@ -44,6 +46,50 @@ This is the whole point (a real handshake, not a schema guess) — but it means
44
46
 
45
47
  ---
46
48
 
49
+ ## MCP Quality Engine
50
+
51
+ `mcp-medic` computes a deterministic **MCP Quality Score** (0–100) from the same diagnostics shown in the report — there's no separate, opaque scoring model guessing independently. Every point deducted traces back to one or more real diagnostics, and the same input always produces the same score (no LLM, no randomness, no extra network calls beyond the MCP inspection already performed).
52
+
53
+ **What the score means — and doesn't**: it's a measurement of what mcp-medic's passive checks actually found (or looked for), not a certification. "Security 93/100" means *mcp-medic's heuristic security checks found issues worth 7 points* — it is not a security audit, and a 100 is not a guarantee the server is safe. Every report includes this disclaimer and a coverage figure so you can tell what was actually inspected (see below).
54
+
55
+ ```bash
56
+ mcp-medic score path/to/config.json
57
+ # or, alongside the normal report:
58
+ mcp-medic check path/to/config.json --score
59
+ ```
60
+
61
+ ### Dimensions
62
+
63
+ | Dimension | Weight | What it reflects |
64
+ |---|---|---|
65
+ | Protocol | 25% | Version negotiation compatibility, capability-inspection health (`resources/list`/`prompts/list` succeeding), `serverInfo` presence, version downgrades |
66
+ | Schema | 20% | `schema.*` diagnostics — malformed/missing input schemas, type mismatches, missing required fields |
67
+ | Agent usability | 20% | Tool/resource/prompt naming, description quality, output schemas, annotations, tool-surface bloat |
68
+ | Security | 20% | `security.*` heuristic diagnostics (untrusted remotes, overbroad permissions, prompt-injection-risk patterns) |
69
+ | Reliability | 15% | Whether the server connects at all (currently binary — see [Known Limitations](#known-limitations)) |
70
+
71
+ These weights are a deliberate, documented choice — not arbitrary — and are not changed casually; see `.agent-room/DECISIONS.md` if you're curious why.
72
+
73
+ ### How deductions work
74
+
75
+ Each dimension starts at 100. Diagnostics are grouped by `checkId` and severity (`error`/`warning`/`info`), each contributing capped points (errors up to 25/checkId, warnings up to 15/checkId, info up to 5/checkId) — so **one noisy check can never dominate a dimension**: 20 tools sharing the same description problem cost at most 15 points, not 300. The report's "Deductions" list names exactly which checks cost how many points. **Nothing is deducted that isn't also a visible diagnostic** — including protocol-level facts like a negotiated version downgrade or a capability that failed to list, which are their own `protocol.*` diagnostics, not a hidden number baked into the score.
76
+
77
+ ### Coverage: what was actually inspected
78
+
79
+ A dimension's score only means something if the checks that produce it actually ran. If you run `mcp-medic` with a custom, restricted check set (`runChecks({ checks: [...] })` via the library API), the score's `coverage` tells you which dimensions were fully evaluated (`'covered'`), partially evaluated (`'partial'` — some but not all of that dimension's checks ran), or not evaluated at all (`'not-covered'`). A `security: 'not-covered'` next to `Security 100/100` means "nothing was looked for," not "nothing is wrong." `coveragePercent` summarizes all five as one number. The standard CLI (`check`/`score`) always runs the full built-in check set, so coverage is 100% there by default.
80
+
81
+ A server that fails to connect is never silently averaged out of a fleet's score either — it's named in `quality.unscoredServers`, and the human report calls it out explicitly.
82
+
83
+ ### Protocol-version-aware quality rules
84
+
85
+ Some things a check might flag could be a genuine protocol violation for a given negotiated MCP version, or merely an ecosystem style recommendation — mcp-medic never mislabels one as the other. `src/protocol/quality-rules.ts` centralizes what each supported version actually requires (today: no version defines a hard tool-name length or character-pattern constraint, so this distinction is currently latent — the abstraction exists so a future version that *does* add one only needs a new entry there, not a rewrite of every check).
86
+
87
+ ### Diagnostic categories
88
+
89
+ Every diagnostic has a stable category (`protocol`, `schema`, `quality`, `security`, `reliability`, `usability`, `configuration`) — set explicitly by newer checks, or inferred from the `checkId` prefix for older ones, so nothing that already worked had to change.
90
+
91
+ ---
92
+
47
93
  ## Quick Start
48
94
 
49
95
  > **Preferred invocation**: Run `npx mcp-medic` (the npm package is `mcp-medic`, not `mcp-doctor`).
@@ -71,6 +117,9 @@ npx mcp-medic check path/to/config.json --policy .mcp-medic-policy.json --export
71
117
  # Display suggested fixes for flagged diagnostics
72
118
  npx mcp-medic check path/to/config.json --show-fixes
73
119
 
120
+ # Compute the deterministic MCP quality score
121
+ npx mcp-medic score path/to/config.json
122
+
74
123
  # Watch mode (re-runs checks on save)
75
124
  npx mcp-medic watch path/to/config.json
76
125
  ```
@@ -87,12 +136,16 @@ npx mcp-medic watch path/to/config.json
87
136
  | `check --registry <id>` | Validate a published registry server directly |
88
137
  | `watch <path>` | Watch configuration file and re-run checks on file save |
89
138
  | `fix <path>` | Interactively apply mechanical suggested fixes (see [Auto-Fix](#auto-fix-mcp-medic-fix) below) |
139
+ | `score <path>` | Print the report with the MCP quality score section (same as `check --score`) |
90
140
  | `--config <path>` | Explicit configuration path |
91
141
  | `--policy <path>` | Apply organizational policy rules (`.mcp-medic-policy.json`) |
92
142
  | `--snapshot <path>` | Compare against baseline snapshot, reporting regressions only |
93
143
  | `--update-snapshot <path>` | Save diagnostic report as new baseline snapshot |
94
144
  | `--export-junit <file>` | Export report in JUnit XML format |
95
- | `--export-json <file>` | Export report in JSON format |
145
+ | `--export-json <file>` | Export report in JSON format (always includes `quality`) |
146
+ | `--export-sarif <file>` | Export report in SARIF 2.1.0 format (GitHub Code Scanning, etc.) |
147
+ | `--score` | Include the MCP quality score section in the human report |
148
+ | `--protocol-version <v>` | MCP protocolVersion to request: `auto` (default) or an explicit version |
96
149
  | `--show-fixes` | Show suggested fixes inline under diagnostics |
97
150
  | `--fail-on <severity>` | Fail with exit code 1 on `error` (default) or `warning` |
98
151
  | `--verbose`, `-v` | Output raw JSON-RPC traffic and debug messages |
@@ -137,10 +190,19 @@ Define organization-wide policies that compose with built-in checks:
137
190
  {
138
191
  "bannedTransports": ["stdio"],
139
192
  "allowedDomains": ["corp.internal", "mcp.example.com"],
140
- "minDescriptionLength": 20
193
+ "minDescriptionLength": 20,
194
+ "quality": {
195
+ "minimumScore": 80,
196
+ "maxTools": 100,
197
+ "requireToolDescriptions": true
198
+ }
141
199
  }
142
200
  ```
143
201
 
202
+ `quality.minimumScore` fails the run (adds an error diagnostic) if the computed MCP Quality Score falls below the threshold. `quality.maxTools` is an org-enforced hard limit — distinct from the built-in `quality.tool-surface` check's default 100-tool *warning*, which stays a recommendation. `quality.requireToolDescriptions` (or the equivalent top-level `requireToolDescriptions`) turns every missing tool description into a policy error rather than the default warning.
203
+
204
+ A `minimumScore` gate is also coverage-aware: if the score was computed from incomplete coverage (or a server that couldn't be scored), a `policy.partial-coverage-with-minimum-score` **warning** is added alongside it — a passing score should never look like a clean bill of health when only part of the server was actually evaluated. This never changes the pass/fail outcome of the `minimumScore` check itself (that stays a plain score-vs-threshold comparison), it just makes a partial assessment visible.
205
+
144
206
  ---
145
207
 
146
208
  ## VS Code Extension (experimental)
@@ -219,7 +281,14 @@ The action adheres to strict exit code taxonomy:
219
281
  | `security.untrusted-remote` | `mcp-medic` | **Official** (heuristic) | Flags non-HTTPS or raw-IP SSE/HTTP server URLs |
220
282
  | `security.overbroad-permissions` | `mcp-medic` | **Official** (heuristic) | Flags tools with unscoped shell/filesystem/network parameters |
221
283
  | `security.prompt-injection-risk` | `mcp-medic` | **Official** (heuristic) | Flags instruction-like language in tool descriptions aimed at the model |
222
- | `policy.*` | `mcp-medic` | **Official** | Evaluates policy-as-code rules (transports, domains, length) |
284
+ | `quality.tool-name` | `mcp-medic` | **Official** | Flags empty/duplicate (error) and overly-long/ambiguous/placeholder (warning) tool names |
285
+ | `quality.vague-description` | `mcp-medic` | **Official** | Flags present-but-vague tool descriptions (placeholder text, single words, name repeated as description) |
286
+ | `quality.output-schema` | `mcp-medic` | **Official** | Flags a malformed `outputSchema` — never flags its absence, which is optional per spec |
287
+ | `quality.tool-annotations` | `mcp-medic` | **Official** | Flags internally contradictory `ToolAnnotations` hints (e.g. both read-only and destructive) |
288
+ | `quality.tool-surface` | `mcp-medic` | **Official** | Flags excessive tool counts (default 100) and near-duplicate names/descriptions |
289
+ | `quality.resource` | `mcp-medic` | **Official** | Flags duplicate/empty resource URIs and missing required `name` from passive `resources/list` results |
290
+ | `quality.prompt` | `mcp-medic` | **Official** | Flags duplicate/empty prompt or argument names and placeholder descriptions from passive `prompts/list` results |
291
+ | `policy.*` | `mcp-medic` | **Official** | Evaluates policy-as-code rules (transports, domains, length, quality thresholds) |
223
292
  | `community.strict-typing` | `mcp-medic-check-strict-typing` | *Planned / example* | Would enforce strict property type annotations |
224
293
  | `community.no-empty-enums` | `mcp-medic-check-no-empty-enums` | *Planned / example* | Would ensure non-empty enum option lists |
225
294
 
@@ -236,6 +305,11 @@ The `security.*` checks are heuristic — they pattern-match on what a server *d
236
305
  - **`security.*` checks are heuristic pattern-matching**, not a security audit — see the note above. They can both miss real issues and flag benign configs (e.g. a legitimate local dev server on plain `http://`).
237
306
  - **Fleet commands (`check-all`, `diff`) are newer and less battle-tested** than `check`/`watch` — the core check pipeline they're built on is the same, but edge cases in glob matching or drift diffing are more likely.
238
307
  - **The VS Code extension and community check packages are not shipped/published** — see the sections above.
308
+ - **The Reliability quality dimension is currently binary**: 100 if the server connected, 0 if it didn't (plus any future `reliability.*` diagnostics — none exist yet). Signals like latency trends, retry behavior, or flakiness across repeated runs aren't scored yet.
309
+ - **`resources/list`/`prompts/list` pagination (`nextCursor`) is not followed** — mcp-medic inspects only the first page a server returns, matching the existing (also unpaginated) `tools/list` handling. A server with a very large resource/prompt catalog behind pagination will be under-inspected.
310
+ - **The quality score never calls `resources/read`, `prompts/get`, or any tool** — it's entirely derived from the passive `initialize`/`tools/list`/`resources/list`/`prompts/list` responses already gathered during a normal `check`. See [SECURITY.md](./SECURITY.md) for the full passive-only guarantee.
311
+ - **The CLI cannot currently restrict the check set** (no `--only-checks` flag), so `coverage` is always 100% via `check`/`score`. Partial coverage (and the coverage-aware `minimumScore` warning) only happens when the library API's `runChecks({ checks: [...] })` is called with a restricted list.
312
+ - **No currently-supported MCP protocol version defines a hard tool-name length or character-pattern constraint**, so `src/protocol/quality-rules.ts`'s protocol-vs-quality distinction for tool names is real but currently dormant — every finding today is a quality recommendation, never a protocol violation, because no version actually requires one. The abstraction is there for when a future version does.
239
313
  - **npm README sync**: Latest docs live on GitHub main; npm README updates on the next publish.
240
314
  - **First run via `npx`** pays a one-time cost to resolve and download the package; once installed (or on a warm npx cache), `--help`/`--version` return in well under 100ms.
241
315
 
@@ -6,4 +6,12 @@ export { sampleCallSimulationCheck } from './sample-call-simulation.js';
6
6
  export { securityUntrustedRemoteCheck } from './security-untrusted-remote.js';
7
7
  export { securityOverbroadPermissionsCheck } from './security-overbroad-permissions.js';
8
8
  export { securityPromptInjectionRiskCheck } from './security-prompt-injection-risk.js';
9
+ export { qualityToolNamesCheck } from './quality-tool-names.js';
10
+ export { qualityToolDescriptionsCheck } from './quality-tool-descriptions.js';
11
+ export { qualityToolOutputSchemaCheck } from './quality-tool-output-schema.js';
12
+ export { qualityToolAnnotationsCheck } from './quality-tool-annotations.js';
13
+ export { qualityToolSurfaceCheck, createToolSurfaceCheck, DEFAULT_MAX_TOOLS_WARNING_THRESHOLD } from './quality-tool-surface.js';
14
+ export { qualityResourcesCheck } from './quality-resources.js';
15
+ export { qualityPromptsCheck } from './quality-prompts.js';
16
+ export { protocolConnectionHealthCheck } from './protocol-connection-health.js';
9
17
  export declare const allChecks: import("../types.js").Check[];
@@ -6,6 +6,14 @@ export { sampleCallSimulationCheck } from './sample-call-simulation.js';
6
6
  export { securityUntrustedRemoteCheck } from './security-untrusted-remote.js';
7
7
  export { securityOverbroadPermissionsCheck } from './security-overbroad-permissions.js';
8
8
  export { securityPromptInjectionRiskCheck } from './security-prompt-injection-risk.js';
9
+ export { qualityToolNamesCheck } from './quality-tool-names.js';
10
+ export { qualityToolDescriptionsCheck } from './quality-tool-descriptions.js';
11
+ export { qualityToolOutputSchemaCheck } from './quality-tool-output-schema.js';
12
+ export { qualityToolAnnotationsCheck } from './quality-tool-annotations.js';
13
+ export { qualityToolSurfaceCheck, createToolSurfaceCheck, DEFAULT_MAX_TOOLS_WARNING_THRESHOLD } from './quality-tool-surface.js';
14
+ export { qualityResourcesCheck } from './quality-resources.js';
15
+ export { qualityPromptsCheck } from './quality-prompts.js';
16
+ export { protocolConnectionHealthCheck } from './protocol-connection-health.js';
9
17
  import { malformedSchemaCheck } from './malformed-schema.js';
10
18
  import { missingRequiredFieldsCheck } from './missing-required-fields.js';
11
19
  import { typeMismatchCheck } from './type-mismatch.js';
@@ -14,7 +22,16 @@ import { sampleCallSimulationCheck } from './sample-call-simulation.js';
14
22
  import { securityUntrustedRemoteCheck } from './security-untrusted-remote.js';
15
23
  import { securityOverbroadPermissionsCheck } from './security-overbroad-permissions.js';
16
24
  import { securityPromptInjectionRiskCheck } from './security-prompt-injection-risk.js';
25
+ import { qualityToolNamesCheck } from './quality-tool-names.js';
26
+ import { qualityToolDescriptionsCheck } from './quality-tool-descriptions.js';
27
+ import { qualityToolOutputSchemaCheck } from './quality-tool-output-schema.js';
28
+ import { qualityToolAnnotationsCheck } from './quality-tool-annotations.js';
29
+ import { qualityToolSurfaceCheck } from './quality-tool-surface.js';
30
+ import { qualityResourcesCheck } from './quality-resources.js';
31
+ import { qualityPromptsCheck } from './quality-prompts.js';
32
+ import { protocolConnectionHealthCheck } from './protocol-connection-health.js';
17
33
  export const allChecks = [
34
+ protocolConnectionHealthCheck,
18
35
  malformedSchemaCheck,
19
36
  missingRequiredFieldsCheck,
20
37
  typeMismatchCheck,
@@ -23,4 +40,11 @@ export const allChecks = [
23
40
  securityUntrustedRemoteCheck,
24
41
  securityOverbroadPermissionsCheck,
25
42
  securityPromptInjectionRiskCheck,
43
+ qualityToolNamesCheck,
44
+ qualityToolDescriptionsCheck,
45
+ qualityToolOutputSchemaCheck,
46
+ qualityToolAnnotationsCheck,
47
+ qualityToolSurfaceCheck,
48
+ qualityResourcesCheck,
49
+ qualityPromptsCheck,
26
50
  ];
@@ -48,6 +48,23 @@ export const malformedSchemaCheck = {
48
48
  },
49
49
  });
50
50
  }
51
+ else if (typeof schemaObj.type === 'string' && schemaObj.type !== 'object') {
52
+ // Per the MCP spec, a tool's inputSchema MUST describe an object
53
+ // (tool arguments are always passed as a JSON object) — a
54
+ // top-level type other than "object" is a protocol violation,
55
+ // not merely a style issue.
56
+ results.push({
57
+ checkId: 'schema.malformed',
58
+ severity: 'error',
59
+ message: `Tool "${tool.name}" inputSchema declares type "${schemaObj.type}", but the MCP spec requires tool inputSchema to be type "object".`,
60
+ serverName: connection.server.name,
61
+ toolName: tool.name,
62
+ details: { inputSchema: schema },
63
+ suggestedFix: {
64
+ description: 'Change inputSchema\'s top-level "type" to "object".',
65
+ },
66
+ });
67
+ }
51
68
  }
52
69
  }
53
70
  catch (err) {
@@ -0,0 +1,17 @@
1
+ import type { Check } from '../types.js';
2
+ /**
3
+ * Surfaces connection-level protocol facts as real, visible diagnostics —
4
+ * previously these (capability-inspection failures, a version downgrade, a
5
+ * missing serverInfo) were deducted from the quality score directly from
6
+ * `MCPConnection` metadata inside src/quality-score.ts, with no
7
+ * corresponding diagnostic a user could actually see in the report or
8
+ * `--json` output. That meant a developer looking at "why did my protocol
9
+ * score drop" would find nothing in `diagnostics` explaining it.
10
+ *
11
+ * This check makes those same facts flow through the standard
12
+ * connection -> diagnostics -> dimension -> score pipeline like every
13
+ * other check, so every point deducted is explainable from a visible
14
+ * DiagnosticResult (see .agent-room/DECISIONS.md for the full rationale,
15
+ * including why this also changes some of the exact point values).
16
+ */
17
+ export declare const protocolConnectionHealthCheck: Check;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Surfaces connection-level protocol facts as real, visible diagnostics —
3
+ * previously these (capability-inspection failures, a version downgrade, a
4
+ * missing serverInfo) were deducted from the quality score directly from
5
+ * `MCPConnection` metadata inside src/quality-score.ts, with no
6
+ * corresponding diagnostic a user could actually see in the report or
7
+ * `--json` output. That meant a developer looking at "why did my protocol
8
+ * score drop" would find nothing in `diagnostics` explaining it.
9
+ *
10
+ * This check makes those same facts flow through the standard
11
+ * connection -> diagnostics -> dimension -> score pipeline like every
12
+ * other check, so every point deducted is explainable from a visible
13
+ * DiagnosticResult (see .agent-room/DECISIONS.md for the full rationale,
14
+ * including why this also changes some of the exact point values).
15
+ */
16
+ export const protocolConnectionHealthCheck = {
17
+ id: 'protocol.connection-health',
18
+ description: 'Flags protocol-level connection facts: a negotiated version downgrade, a missing serverInfo, or a declared capability whose list call failed.',
19
+ run(connection) {
20
+ const results = [];
21
+ try {
22
+ if (connection.protocolVersion?.negotiated &&
23
+ connection.protocolVersion.negotiated !== connection.protocolVersion.requested) {
24
+ results.push({
25
+ checkId: 'protocol.version-downgrade',
26
+ severity: 'info',
27
+ message: `Server negotiated protocol version ${connection.protocolVersion.negotiated} instead of the requested ${connection.protocolVersion.requested}.`,
28
+ serverName: connection.server.name,
29
+ category: 'protocol',
30
+ details: {
31
+ requested: connection.protocolVersion.requested,
32
+ negotiated: connection.protocolVersion.negotiated,
33
+ },
34
+ });
35
+ }
36
+ if (!connection.serverInfo?.name) {
37
+ results.push({
38
+ checkId: 'protocol.missing-server-info',
39
+ severity: 'info',
40
+ message: 'Server did not report its name/version in serverInfo during initialize.',
41
+ serverName: connection.server.name,
42
+ category: 'protocol',
43
+ suggestedFix: { description: 'Have the server include a serverInfo.name in its initialize response.' },
44
+ });
45
+ }
46
+ if (connection.capabilityErrors?.resources) {
47
+ results.push({
48
+ checkId: 'protocol.capability-error',
49
+ severity: 'error',
50
+ message: `Server declared the "resources" capability, but resources/list failed: ${connection.capabilityErrors.resources}`,
51
+ serverName: connection.server.name,
52
+ category: 'protocol',
53
+ details: { capability: 'resources', error: connection.capabilityErrors.resources },
54
+ });
55
+ }
56
+ if (connection.capabilityErrors?.prompts) {
57
+ results.push({
58
+ checkId: 'protocol.capability-error',
59
+ severity: 'error',
60
+ message: `Server declared the "prompts" capability, but prompts/list failed: ${connection.capabilityErrors.prompts}`,
61
+ serverName: connection.server.name,
62
+ category: 'protocol',
63
+ details: { capability: 'prompts', error: connection.capabilityErrors.prompts },
64
+ });
65
+ }
66
+ }
67
+ catch (err) {
68
+ results.push({
69
+ checkId: 'protocol.connection-health',
70
+ severity: 'error',
71
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
72
+ serverName: connection.server.name,
73
+ category: 'protocol',
74
+ });
75
+ }
76
+ return results;
77
+ },
78
+ };
@@ -0,0 +1,8 @@
1
+ import type { Check } from '../types.js';
2
+ /**
3
+ * Inspects only what `prompts/list` already returned (see
4
+ * MCPConnection.prompts, populated passively in src/protocol/connect.ts).
5
+ * Never calls `prompts/get` — that would retrieve/render the prompt, out
6
+ * of scope for a passive `check`.
7
+ */
8
+ export declare const qualityPromptsCheck: Check;
@@ -0,0 +1,121 @@
1
+ /** Same conservative placeholder list used for tool descriptions. */
2
+ const PLACEHOLDER_DESCRIPTIONS = new Set([
3
+ 'todo',
4
+ 'test',
5
+ 'foo',
6
+ 'bar',
7
+ 'description',
8
+ 'prompt',
9
+ 'tbd',
10
+ 'n/a',
11
+ 'na',
12
+ 'none',
13
+ 'placeholder',
14
+ 'xxx',
15
+ ]);
16
+ /**
17
+ * Inspects only what `prompts/list` already returned (see
18
+ * MCPConnection.prompts, populated passively in src/protocol/connect.ts).
19
+ * Never calls `prompts/get` — that would retrieve/render the prompt, out
20
+ * of scope for a passive `check`.
21
+ */
22
+ export const qualityPromptsCheck = {
23
+ id: 'quality.prompt',
24
+ description: 'Flags duplicate/empty prompt names, placeholder descriptions, and malformed argument definitions in prompts/list results.',
25
+ run(connection) {
26
+ const results = [];
27
+ try {
28
+ const prompts = connection.prompts;
29
+ if (!prompts || !Array.isArray(prompts)) {
30
+ return results;
31
+ }
32
+ const byName = new Map();
33
+ for (const prompt of prompts) {
34
+ const name = prompt.name;
35
+ if (typeof name !== 'string' || name.trim() === '') {
36
+ results.push({
37
+ checkId: 'quality.prompt',
38
+ severity: 'error',
39
+ message: 'Prompt has an empty or invalid "name" — the MCP spec requires prompts to have a name.',
40
+ serverName: connection.server.name,
41
+ category: 'schema',
42
+ details: { prompt },
43
+ });
44
+ continue;
45
+ }
46
+ byName.set(name, (byName.get(name) ?? 0) + 1);
47
+ const description = prompt.description?.trim();
48
+ if (!description) {
49
+ results.push({
50
+ checkId: 'quality.prompt',
51
+ severity: 'info',
52
+ message: `Prompt "${name}" has no description, making it harder for an agent to know when to use it.`,
53
+ serverName: connection.server.name,
54
+ category: 'quality',
55
+ });
56
+ }
57
+ else if (PLACEHOLDER_DESCRIPTIONS.has(description.toLowerCase())) {
58
+ results.push({
59
+ checkId: 'quality.prompt',
60
+ severity: 'warning',
61
+ message: `Prompt "${name}" description ("${description}") looks like placeholder text.`,
62
+ serverName: connection.server.name,
63
+ category: 'quality',
64
+ confidence: 'high',
65
+ });
66
+ }
67
+ if (Array.isArray(prompt.arguments)) {
68
+ const argNames = new Map();
69
+ for (const arg of prompt.arguments) {
70
+ if (!arg.name || arg.name.trim() === '') {
71
+ results.push({
72
+ checkId: 'quality.prompt',
73
+ severity: 'error',
74
+ message: `Prompt "${name}" has an argument with an empty or missing "name" — required by the MCP spec's PromptArgument type.`,
75
+ serverName: connection.server.name,
76
+ category: 'schema',
77
+ details: { promptName: name, argument: arg },
78
+ });
79
+ continue;
80
+ }
81
+ argNames.set(arg.name, (argNames.get(arg.name) ?? 0) + 1);
82
+ }
83
+ for (const [argName, count] of argNames) {
84
+ if (count > 1) {
85
+ results.push({
86
+ checkId: 'quality.prompt',
87
+ severity: 'error',
88
+ message: `Prompt "${name}" declares argument "${argName}" ${count} times.`,
89
+ serverName: connection.server.name,
90
+ category: 'schema',
91
+ details: { promptName: name, argumentName: argName, duplicateCount: count },
92
+ });
93
+ }
94
+ }
95
+ }
96
+ }
97
+ for (const [name, count] of byName) {
98
+ if (count > 1) {
99
+ results.push({
100
+ checkId: 'quality.prompt',
101
+ severity: 'error',
102
+ message: `Prompt name "${name}" is declared ${count} times — a client cannot reliably invoke a specific one by name.`,
103
+ serverName: connection.server.name,
104
+ category: 'schema',
105
+ details: { duplicateCount: count },
106
+ });
107
+ }
108
+ }
109
+ }
110
+ catch (err) {
111
+ results.push({
112
+ checkId: 'quality.prompt',
113
+ severity: 'error',
114
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
115
+ serverName: connection.server.name,
116
+ category: 'quality',
117
+ });
118
+ }
119
+ return results;
120
+ },
121
+ };
@@ -0,0 +1,8 @@
1
+ import type { Check } from '../types.js';
2
+ /**
3
+ * Inspects only what `resources/list` already returned (see
4
+ * MCPConnection.resources, populated passively in src/protocol/connect.ts).
5
+ * Never calls `resources/read` — that would be real content retrieval, out
6
+ * of scope for a passive `check`.
7
+ */
8
+ export declare const qualityResourcesCheck: Check;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Inspects only what `resources/list` already returned (see
3
+ * MCPConnection.resources, populated passively in src/protocol/connect.ts).
4
+ * Never calls `resources/read` — that would be real content retrieval, out
5
+ * of scope for a passive `check`.
6
+ */
7
+ export const qualityResourcesCheck = {
8
+ id: 'quality.resource',
9
+ description: 'Flags duplicate/empty resource URIs, missing required names, and other resources/list quality issues.',
10
+ run(connection) {
11
+ const results = [];
12
+ try {
13
+ const resources = connection.resources;
14
+ if (!resources || !Array.isArray(resources)) {
15
+ return results;
16
+ }
17
+ const byUri = new Map();
18
+ for (const resource of resources) {
19
+ const uri = resource.uri;
20
+ if (typeof uri !== 'string' || uri.trim() === '') {
21
+ results.push({
22
+ checkId: 'quality.resource',
23
+ severity: 'error',
24
+ message: 'Resource has an empty or invalid "uri" — the MCP spec requires resources to have a URI.',
25
+ serverName: connection.server.name,
26
+ category: 'schema',
27
+ details: { resource },
28
+ });
29
+ continue;
30
+ }
31
+ byUri.set(uri, (byUri.get(uri) ?? 0) + 1);
32
+ // Per the MCP spec's Resource type (extends BaseMetadata), "name" is
33
+ // a required field, not optional.
34
+ if (!resource.name || resource.name.trim() === '') {
35
+ results.push({
36
+ checkId: 'quality.resource',
37
+ severity: 'error',
38
+ message: `Resource "${uri}" is missing a "name" — required by the MCP spec's Resource type.`,
39
+ serverName: connection.server.name,
40
+ category: 'schema',
41
+ details: { uri },
42
+ suggestedFix: { description: `Add a "name" field to the resource at "${uri}".` },
43
+ });
44
+ }
45
+ // description is optional per spec — absence is a quality
46
+ // recommendation, not a violation.
47
+ if (!resource.description || resource.description.trim() === '') {
48
+ results.push({
49
+ checkId: 'quality.resource',
50
+ severity: 'info',
51
+ message: `Resource "${uri}" has no description, making it harder for an agent to know when to read it.`,
52
+ serverName: connection.server.name,
53
+ category: 'quality',
54
+ details: { uri },
55
+ });
56
+ }
57
+ if (resource.size !== undefined && (typeof resource.size !== 'number' || resource.size < 0)) {
58
+ results.push({
59
+ checkId: 'quality.resource',
60
+ severity: 'warning',
61
+ message: `Resource "${uri}" declares an invalid "size" (${JSON.stringify(resource.size)}) — size must be a non-negative number.`,
62
+ serverName: connection.server.name,
63
+ category: 'schema',
64
+ details: { uri, size: resource.size },
65
+ });
66
+ }
67
+ }
68
+ for (const [uri, count] of byUri) {
69
+ if (count > 1) {
70
+ results.push({
71
+ checkId: 'quality.resource',
72
+ severity: 'error',
73
+ message: `Resource URI "${uri}" is declared ${count} times — resource URIs must be unique.`,
74
+ serverName: connection.server.name,
75
+ category: 'schema',
76
+ details: { uri, duplicateCount: count },
77
+ });
78
+ }
79
+ }
80
+ }
81
+ catch (err) {
82
+ results.push({
83
+ checkId: 'quality.resource',
84
+ severity: 'error',
85
+ message: `check failed internally: ${err instanceof Error ? err.message : String(err)}`,
86
+ serverName: connection.server.name,
87
+ category: 'quality',
88
+ });
89
+ }
90
+ return results;
91
+ },
92
+ };
@@ -0,0 +1,10 @@
1
+ import type { Check } from '../types.js';
2
+ /**
3
+ * MCP `ToolAnnotations` are optional client *hints* the spec explicitly
4
+ * does not treat as authoritative — a server can declare `readOnlyHint:
5
+ * true` and still do something destructive. This check only flags
6
+ * internally self-contradictory combinations of hints (the server's own
7
+ * declaration doesn't add up), never infers "this tool is dangerous" from
8
+ * an annotation, and never treats annotations as a security guarantee.
9
+ */
10
+ export declare const qualityToolAnnotationsCheck: Check;