@vantageos/mcp-doctor 0.2.0 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -3,6 +3,74 @@
3
3
  All notable changes to `@vantageos/mcp-doctor` are documented here.
4
4
  This project adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 0.3.1 — 2026-10-04
7
+
8
+ ### Fixed
9
+ - **`npm test` on a clean clone failed 3 of 341.** `tests/bin-entrypoint.test.ts`
10
+ symlinks to `dist/doctor.js` and invokes it — deliberately, because the symlink is
11
+ the trigger for the defect those cases cover — but it never ensured the build
12
+ existed. A contributor cloning the repo and running the suite got
13
+ `Cannot find module .../dist/doctor.js` three times, on `main`, with nothing saying
14
+ a build was required first.
15
+ Fixed with `pretest: npm run build`, following the convention `prepack` and
16
+ `prepublishOnly` already use in this manifest rather than inventing a mechanism.
17
+ - **D9 no longer throws when no `package.json` sits above the scanned root.** It
18
+ returns `could-not-judge` in band, the run completes the remaining detectors, and
19
+ the table prints. Previously the throw reached the consumer as exit 1 with a stack
20
+ trace — exit 1 being this tool's code for *a violation*, so a crash was
21
+ indistinguishable from a finding.
22
+
23
+ ### Note on why 0.3.0 shipped with the test defect
24
+ Every environment where the work happens already has `dist/` — so the test passed for
25
+ everyone who wrote it, and the one gated path (`prepublishOnly`, which runs
26
+ `build && test`) was the one path that could not expose it. A test depending on an
27
+ unstated prior step cannot fail for its author.
28
+
29
+ ---
30
+
31
+ ## 0.3.0 — 2026-10-04
32
+
33
+ The documented invocation never ran. Through `node_modules/.bin/mcp-doctor` this
34
+ tool printed **nothing** and exited **0** — for every target, since the bin was
35
+ published. Every station that ran `npx mcp-doctor <tree>` and saw no output read a
36
+ pass that was never computed.
37
+
38
+ ### READ THIS BEFORE UPGRADING
39
+
40
+ **A CI job that was passing may now fail, and that is the fix working.** If you
41
+ wired `npx mcp-doctor` into a pipeline and it has been green, it was green because
42
+ nothing ran. On a real target this tool has findings to report. Budget for a first
43
+ red run.
44
+
45
+ ### Fixed
46
+ - **The bin is a symlink, and the entrypoint guard compared raw paths.** npm installs
47
+ `bin` as a symlink, so `process.argv[1]` is the link path while `import.meta.url`
48
+ is the resolved real path; compared directly they never match and `main()` never
49
+ ran. Both sides now resolve through `realpathSync`, each side guarded: a path that
50
+ cannot be resolved answers "do not run" rather than either running on an ordinary
51
+ library import or failing silently.
52
+ Measured on the published 0.2.0 before the fix — `./node_modules/.bin/mcp-doctor t`
53
+ exit 0 / 0 bytes against `node .../dist/doctor.js t` exit 2 / 6308 bytes.
54
+ - **`--version`, `--help` and `-V` were silent for the same reason** and now answer.
55
+ The version is READ from the shipped manifest rather than compiled in, so it cannot
56
+ drift from the binary.
57
+ - **The usage line named a development command no consumer can type**
58
+ (`tsx src/doctor.ts`). It now names `mcp-doctor` and lists the exit codes.
59
+
60
+ ### Added
61
+ - **D16 is judged at runtime, not only statically.** Health-route census plus a dynamic
62
+ mode: the version a server actually SERVES on its pre-auth `initialize` handshake is
63
+ read from `serverInfo.version` and compared against `package.json`. A served version
64
+ that drifts from the canonical one is now a finding rather than invisible.
65
+ - A static D16 verdict says out loud which mode produced it (`[static read]` prefix), so
66
+ a reader can tell an observation from a fallback.
67
+
68
+ ### Changed — observable contract
69
+ - D16's static verdict strings carry the `[static read]` prefix. Anything parsing that
70
+ text rather than the `state` field must be updated.
71
+
72
+ ---
73
+
6
74
  ## 0.2.0 — 2026-08-16
7
75
 
8
76
  The engine gains a live-session contract and an honest exit code; a second
package/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # @vantageos/mcp-doctor
2
2
 
3
- Static **MCP conformance checker**. Point it at an MCP server source tree and it emits a
4
- **conformance-diff table** against the MCP **2026-07-28** standard — one row per checked item:
3
+ **MCP conformance checker**. Point it at an MCP server source tree and it emits a
4
+ **conformance-diff table** against the MCP **2026-07-28** standard — one row per detector:
5
5
 
6
6
  ```
7
- item | file:line | present | absent | non-conforming | verdict
7
+ item | file:line | state | verdict
8
8
  ```
9
9
 
10
- No narrative, no LLM, no network: a deterministic static scan you can wire into CI. It reproduces,
10
+ where `state` is one of `present`, `absent`, `non-conforming` or `could-not-judge` (see
11
+ [States and exit codes](#states-and-exit-codes)).
12
+
13
+ No narrative, no LLM. By default it is a deterministic **static** scan you can wire into CI; with
14
+ `--mode=dynamic` it also launches the server and judges what it actually serves. It reproduces,
11
15
  unaided, the manual conformance audits the fleet ran by hand on its own MCP servers.
12
16
 
13
17
  ## Usage
@@ -37,6 +41,56 @@ D7 protocol-kit | package.json | absent | hand-rolled bu
37
41
  The path is the root of the MCP server sources (the folder holding `server.ts` / `transport/` /
38
42
  `package.json`). `node_modules`, `dist`, `build`, `.git` and test folders are skipped automatically.
39
43
 
44
+ ### Options
45
+
46
+ The binary's own `--help` is the authority; this is what it prints:
47
+
48
+ ```
49
+ usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both]
50
+
51
+ --mode=static read the tree only (default)
52
+ --mode=dynamic launch the server and judge what it serves
53
+ --mode=both both, dynamic findings taking precedence
54
+ --version print the version and exit
55
+ --help print this and exit
56
+
57
+ exit: 0 clean, 1 a violation, 2 an abstention with no violation
58
+ ```
59
+
60
+ ## States and exit codes
61
+
62
+ | state | meaning | effect on exit |
63
+ |---|---|---|
64
+ | `present` | the standard item is satisfied | none |
65
+ | `absent` | the detector found nothing to judge — see the caveat below | none |
66
+ | `non-conforming` | the item is present and wrong | **exit 1** |
67
+ | `could-not-judge` | the detector could not read its input and says so in the row — in 0.3.1, only D9 does this (no `package.json` above the root); see the limits below | **exit 2**, unless something else is non-conforming |
68
+
69
+ The exit code keeps three outcomes distinct, which is what makes the tool safe in CI:
70
+
71
+ - **0** — clean.
72
+ - **1** — at least one violation.
73
+ - **2** — no violation, but at least one detector abstained. **An abstention is never a pass**: it
74
+ means part of the tree was not judged, and the row names which part and why.
75
+
76
+ **Two known limits in 0.3.1, stated rather than hidden:**
77
+
78
+ - **`absent` currently means two things.** It is used both for "this rule does not apply here" and,
79
+ for some detectors, "this is required and missing". The second should move the exit code and
80
+ today does not. Until that is split, read an `absent` row against the rule it names rather than
81
+ treating it as harmless.
82
+ - **Unreadable input is handled in band by D9 only.** In 0.3.1, D9 reports `could-not-judge` when no
83
+ `package.json` sits above the scanned root, and the run completes. Several other detectors still
84
+ **throw** on unreadable input — a malformed manifest, a computed value — which aborts the run with
85
+ exit 1 and a stack trace, indistinguishable from a violation. Converting them is in progress.
86
+
87
+ By design, a detector whose own internal self-check fails stops the run — that is the instrument
88
+ reporting that it is broken, and silencing it would hide the fault behind a normal-looking table.
89
+
90
+ **Upgrading from 0.2.0:** through `npx` or `node_modules/.bin`, 0.2.0 printed nothing and exited 0
91
+ for every target, because its entrypoint guard never fired under a symlink. A CI job that has been
92
+ green on 0.2.0 was green because nothing ran. Expect findings on the first run of 0.3.x.
93
+
40
94
  ## Programmatic use
41
95
 
42
96
  ```ts
@@ -46,7 +100,7 @@ const results = runDoctor("./mcp-server");
46
100
  console.log(renderTable("./mcp-server", results));
47
101
  ```
48
102
 
49
- ## What it checks — the 6 Tier-A detectors (MCP 2026-07-28)
103
+ ## What it checks (MCP 2026-07-28)
50
104
 
51
105
  Each detector maps one standard item and is proven **bipolar** on real material: a genuine defect
52
106
  (MUST_FLAG) and a genuine conformant site (MUST_PASS).
@@ -62,7 +116,11 @@ Each detector maps one standard item and is proven **bipolar** on real material:
62
116
  | **D15** cache-coherence | Server Rule 15 — every tool-set mutation (RegisteredTool `.enable/.disable/.update/.remove`, `unregisterTool`) is correlated with a `notifications/tools/list_changed` emission | present / absent / non-conforming / could-not-judge |
63
117
  | **D16** version-observable | Server Rule 16 — `server/discover` (or an HTTP `/health` route) exposes a `version` field sourced from `package.json`, reachable pre-auth | present / absent / non-conforming / could-not-judge |
64
118
 
65
- The registered detector set is the source of truth (`src/detectors/index.ts`); the table above is illustrative, not the count. **Adding a detector is how a friction becomes durable — see [CONTRIBUTING.md](./CONTRIBUTING.md) for the loop.**
119
+ **This table is a sample, not the inventory.** Every registered detector runs on every invocation,
120
+ so the authoritative list is simply the output of a run — one row per detector. The registered set
121
+ lives in `src/detectors/index.ts`. No count is written here on purpose: a number typed into a README
122
+ is correct on the day it is written and wrong at the next detector, which is exactly how this
123
+ heading came to claim 6 while the tool shipped dozens. **Adding a detector is how a friction becomes durable — see [CONTRIBUTING.md](./CONTRIBUTING.md) for the loop.**
66
124
 
67
125
  ## Positioning
68
126
 
@@ -71,10 +129,17 @@ standard by naming the exact `file:line` to fix — and a **vendable product** f
71
129
  MCP servers who needs a reproducible conformance gate. It is deterministic: the same tree yields the
72
130
  same diff on any machine, so it belongs in CI, not in a review checklist.
73
131
 
132
+ ## Contributing
133
+
134
+ `npm test` builds first (a `pretest` script runs `npm run build`), because several tests drive the
135
+ real published entry point through a symlinked bin and need `dist/` to exist. Earlier versions
136
+ assumed a prior build, which passed for anyone who had already built and failed for anyone who had
137
+ just cloned.
138
+
74
139
  ## License
75
140
 
76
141
  FSL-1.1-Apache-2.0 (Functional Source License, Apache 2.0 future license). See [LICENSE](./LICENSE).
77
142
 
78
143
  ---
79
144
 
80
- Orchestrator: Gamma — VantageOS Team | 2026-08-03
145
+ Orchestrator: Gamma — VantageOS Team | 2026-10-04
package/dist/doctor.d.ts CHANGED
@@ -37,6 +37,13 @@ interface DetectorResult {
37
37
  interface RuntimeTool {
38
38
  /** The tool name the server actually advertises at runtime (post-startup). */
39
39
  name: string;
40
+ /**
41
+ * The description the server actually SERVES over `tools/list`, post-
42
+ * startup — the surface C-19 measures (never the source-declared string,
43
+ * which can drift from what a real client receives). Unset only when the
44
+ * running server declares no description for the tool.
45
+ */
46
+ description?: string;
40
47
  /**
41
48
  * Number of eval cases covering this tool, resolved by the runtime builder
42
49
  * from the target's `evals/evals.json` (matched by tool name). Optional
@@ -65,9 +72,42 @@ interface RuntimeContext {
65
72
  root: string;
66
73
  tools: RuntimeTool[];
67
74
  launchError?: string;
75
+ /**
76
+ * The `serverInfo` object the running server returned in its `initialize`
77
+ * response — the same non-authenticated discovery handshake a client reads
78
+ * `version` from (S-7). Additive field: existing dynamic detectors that
79
+ * never read it are unaffected. C-25 reads it for a build-identity field
80
+ * served alongside `version` (commit hash / build id). Unset when the
81
+ * launched server returned no `serverInfo` or the launch failed
82
+ * (`launchError` is then set instead).
83
+ */
84
+ serverInfo?: Record<string, unknown>;
85
+ /**
86
+ * The commit the running server is EXPECTED to be serving, DERIVED from
87
+ * `git rev-parse HEAD` in the target tree — never hand-typed (see
88
+ * derive-never-type). Unset when the target is not a resolvable git
89
+ * checkout: C-25's dynamic served-vs-expected comparison is then reported
90
+ * as OWED (could-not-judge), never silently skipped as a pass.
91
+ * When set it is ALWAYS a full 40-hex commit id (`/^[0-9a-f]{40}$/`,
92
+ * enforced by `deriveExpectedCommit`): C-25's comparison reasons about
93
+ * length and asserts that shape rather than assuming it.
94
+ */
95
+ expectedCommit?: string;
68
96
  }
69
97
  interface Detector {
70
98
  id: string;
99
+ /**
100
+ * The canonical standard rule id this detector judges (e.g. "C-6"). Optional
101
+ * so the pre-existing detectors that predate this field still compile; every
102
+ * detector built or re-pinned against a standard rule MUST populate it.
103
+ * Coverage of the standard is DERIVED from this field across the registry
104
+ * (see `scripts/trunk-coverage.ts`) — never from a hand-kept list.
105
+ */
106
+ rule?: string;
107
+ /** The standard commit this detector's `rule` was read against. Required
108
+ * alongside `rule`: a rule id with no pin is unverifiable against a moving
109
+ * doc. */
110
+ pin?: string;
71
111
  /** Static verdict from the source tree. REQUIRED — static is never optional. */
72
112
  run(tree: Tree): DetectorResult;
73
113
  /**
@@ -129,6 +169,10 @@ interface Session {
129
169
  */
130
170
  interface LiveDetector {
131
171
  id: string;
172
+ /** See `Detector.rule` — same contract, same derivation. */
173
+ rule?: string;
174
+ /** See `Detector.pin` — same contract. */
175
+ pin?: string;
132
176
  /**
133
177
  * Live verdict from a running session. REQUIRED for a live detector. It owns
134
178
  * the abstain contract: a session it cannot read (absent, throwing, timing