@vantageos/mcp-doctor 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -3,6 +3,84 @@
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.7.0 — 2026-10-10
7
+
8
+ Measured against the ElPi-Corp standard pinned at `b714129a110237c5521d5269ee61ab919408a17f`
9
+ (91 rules; ElPi-Corp #243 on top of `2c4dedbe`). Every new or changed detector has a RED test
10
+ committed before its implementation, a bite mutation on foreign material and the legitimate form of
11
+ the same edit (`scripts/v070-foreign-bite-probe.sh`), and its restore proven (`diff -r` empty).
12
+
13
+ New rule ids, derived (`git diff 2c4dedbe b714129a` over the four standards, ids of the numbered
14
+ rules): **C-29 C-30 C-31 C-32 C-33 · S-29 S-30 S-31 S-32 S-33 S-34 · E-7 E-8 E-9 E-10 E-11 E-12 ·
15
+ A-8 A-9 A-10 A-11 A-12**, plus the **S-5 elements (f)..(l)**. The amended-in-place rules are C-2,
16
+ C-6 (debt), C-15, C-16, S-3, S-4, S-10, S-12, E-1, E-3, A-2, A-7.
17
+
18
+ ### Added — one detector per new rule
19
+ - **C29 served-skills**, **C30 skills-org-bound**, **C31 plugin-installable**, **C32
20
+ host-key-additive**, **C33 identity-tool**.
21
+ - **S29 events-declared**, **S30 event-descriptors**, **S31 subscription-contract**, **S32
22
+ callback-verification**, **S33 signed-delivery**, **S34 access-recheck**.
23
+ - **S5 authz-elements (f)-(l)** — PRM `resource` and suffixed route, CIMD + `"none"`, `iss`,
24
+ `/authorize` authenticating a person, jwt-bearer, OIDC discovery.
25
+ - **E7** middleware-events, **E8** official-ext-declared, **E9** host-invoked-tools, **E10**
26
+ mrtr-only, **E11** surfaces-derived, **E12** host-capability-placement.
27
+ - **A8** ui-skills-disjoint, **A9** entrypoint, **A10** display-modes, **A11**
28
+ sandbox-domain-derived, **A12** file-write.
29
+ - **What a dynamique row returns on a static run.** `absent` only when the subject is statically
30
+ proven absent (the matcher carries a positive control proving it can see the declaration); a red
31
+ where the tree contradicts the rule; otherwise `could-not-judge` naming the half that is owed —
32
+ never `present`, and never `absent` for a surface it found. The dynamic halves (a served
33
+ `tools/list`, two scoped identities, a webhook receiver) are not implemented; each detector's
34
+ header and verdict name what is owed.
35
+ - **`VERDICT DUE (C-23)`**: S-5 (i) and (j) are partly judgement. The detector exposes the sites and
36
+ returns `could-not-judge` with that prefix; the four states cannot say "sites exposed, verdict due",
37
+ and C-23 forbids `present` and `absent` there. Reported as a missing fifth state.
38
+
39
+ ### Added — amended rules, adapted in place
40
+ - **C-2**: a tool name over 64 characters is red (static and runtime names).
41
+ - **C-15**: a tool without a `title` (its own field, else `annotations.title`) is red; an enumerated
42
+ parameter holding a read verb and a write verb of the closed set (§3) is red.
43
+ - **C-16**: a `payloadSchema` string field named as a free body next to a read tool is red.
44
+ - **S-3**: when Skills is declared, `skills/list` and `skills/get` must carry `ttlMs`/`cacheScope`.
45
+ - **S-4**: `server/discover` is required on the closed trigger (manifest `minProtocolVersion >=
46
+ 2026-07-28`, the `events` capability, a host extension); with none of them the rule does not oblige.
47
+ An SDK v2 dependency (it serves `server/discover`) makes a missing literal `could-not-judge`.
48
+ - **E-1** and **`vendor-namespaces.json`** at the repository root (`openai/` first member, plus the
49
+ keys owned by another rule), shipped in `files` and read at the package's own version; an
50
+ unreadable registry is `could-not-judge`. A vendor key under `_meta`, `capabilities.extensions` or
51
+ `capabilities.experimental` is admitted only if its namespace is a member; an extension identifier
52
+ under a vendor namespace is red (we never publish under one).
53
+ - **E-3**: the fourth type, `host-capability-adapter`.
54
+ - **S-12**, **A-2**, **A-7** had no detector; they have one now (S-10 still has none: its only new
55
+ reading is a dynamic loopback receiver).
56
+ - **C-6 debt (named by the reviewer on #54)**: a log of an environment variable naming a URL /
57
+ endpoint / host, or of `process.cwd()`, is Form D. The class is swept: `os.homedir()`,
58
+ `os.tmpdir()`, `process.execPath`, `import.meta.dirname|filename|url`; `process.stderr|stdout.write`
59
+ counts as a log call; the value is followed through an identifier, a destructured binding and an
60
+ object property. A non-URL variable (`NODE_ENV`) and a read that is not logged stay green.
61
+
62
+ ### Changed
63
+ - Every detector now carries `rule` + `pin` = the standard pin above (the detectors that predate the
64
+ restructure were mapped to their current ids: D2/D12 S-1, D3 S-2, D15 S-6, D16 S-7, D6 A-6,
65
+ DServer8 A-1, DServer14 and DLocalIdentity S-5, DExt5 C-1, DExt7 C-5, DExt8 C-3, DExt9 E-6).
66
+ Re-pinning is not a retype: for each, the rule text is byte-identical between the detector's old
67
+ pin and the new one (C-17, C-18, C-25, C-26, S-28: identical between `2c4dedbe` and `b714129a`).
68
+ - `scripts/trunk-coverage.ts` is replaced by `scripts/standard-coverage.ts` (four standards, genre
69
+ column read from the detector table). `analysis/mcp-doctor-coverage-grid.md` is regenerated from it.
70
+ - Fixtures updated for rules that now require more: a `title` on every tool of the green C-15
71
+ fixtures; the oracle fixtures carry a plugin folder (C-31 is `could-not-judge` without one).
72
+ - A tool-method literal that is an object KEY (a cache-hints table) is not a handler.
73
+
74
+ ### Fixed (found by the foreign bite probe)
75
+ - A-2 credited a single-file plugin that was only NAMED in a comment; comments are now ignored.
76
+ - S-5 (f) followed only the first `resource:` near `authorization_servers`; every PRM site is judged
77
+ and a member read (`s.resource`) is followed to its definitions.
78
+
79
+ ### Measured (`--mode=static`, doctor 0.6.0 -> 0.7.0)
80
+ - `vantage-mcp-boilerplate` @ `cdb14add`: 10 -> 11 non-conforming, 2 -> 6 could-not-judge.
81
+ - `vantage-peers` `mcp-server` @ `9682a0d8`: 11 -> 14 non-conforming, 3 -> 7 could-not-judge.
82
+ - Every new non-conforming / could-not-judge line is named and classified in the pull request.
83
+
6
84
  ## 0.6.0 — 2026-10-10
7
85
 
8
86
  Rebased onto 0.5.0 (S-28, PR #53); this release ships as 0.6.0.
package/README.md CHANGED
@@ -125,6 +125,49 @@ Each detector maps one standard item and is proven **bipolar** on real material:
125
125
  | **DLocalIdentity** no-local-identity | No MCP server defines its own identity primitive: resolving a caller to a tenant or agent (local JWT verify, a populated allow-list attached to a bearer), asserting org membership or role, filtering rows by caller scope (`fromAllowList`, a name allow-list, a row's `orgId` or acting-name field such as `createdBy` / `assignedTo` compared to the caller's, by `===`, `==`, `Object.is` or after `toLowerCase()`), or validating a credential (local `timingSafeEqual`, a digest of a presented value, HMAC; a hash over a static literal is not a site). A call to a binding imported from `@vantageos/cloud-identity` is not a site. Minting/signing sites (`new SignJWT`, `jwt.sign`, a private-key import) are listed as `MINTING/SIGNING` and not counted; verification and claim reads stay violations whoever issued the token (no issuer carve-out). | present (0, or EXEMPT) / non-conforming (count > ceiling, default 0) / could-not-judge |
126
126
  | **S-28** claim-forwarded | Server complement S-28 (standard `bacc54a`) — a tool forwards the verified claim of its caller (`verifiedOrg` / `verifiedPerson`) to every backend door it calls and refuses when it holds none. Reads each `<client>.query\|mutation\|action` call, the helpers it spreads, the refusal before it, and the backend source's door list (`--backend=<convex-dir>`, else the nearest `convex/` above the tree). A door that is service-account-only or declared `fleet-wide:` needs no claim; no backend found, an unknown door, a computed door name are could-not-judge. | present / non-conforming / could-not-judge / absent (no Convex client imported) |
127
127
 
128
+ ### Detectors added in 0.7.0 (standard `b714129a`, ElPi-Corp #243)
129
+
130
+ One detector per rule that #243 added, plus the S-5 elements (f)..(l), each implementing the
131
+ detector row of its rule. "Static run" is what the default mode returns; a dynamique row never
132
+ returns `present` statically — it returns `absent` only when its subject is proven absent, a red
133
+ where the tree contradicts the rule, and otherwise `could-not-judge` naming the half that is owed.
134
+
135
+ | ID | Rule | The static run reads | Verdicts |
136
+ |----|------|----------------------|----------|
137
+ | **C29** served-skills | C-29 | the `io.modelcontextprotocol/skills` declaration, `skills/list` + `skills/get` handlers, the `resources` capability, every `SKILL.md` `name` against its directory, a `"dynamic"` catalog manifest, `ttlMs`/`cacheScope` | absent (aloud, naming any unserved SKILL.md) / present + dynamic half owed / non-conforming |
138
+ | **C30** skills-org-bound | C-30 | whether Skills is declared | absent / could-not-judge (two scoped, non-master identities owed) |
139
+ | **C31** plugin-installable | C-31 | every `.claude-plugin/plugin.json` folder: kebab-case name, version equal to `package.json`, licence, README >= 40 words outside code, `.mcp.json` https `type: http` entries without a literal credential, pinned launchers, no top-level `bin/`, no path leaving the folder, no credential shape in README | present / non-conforming / could-not-judge (no plugin folder, unparseable file, env-built URL without default) |
140
+ | **C32** host-key-additive | C-32 | `_meta` objects (followed through spreads and consts): `openai/outputTemplate` needs `ui.resourceUri`, `openai/visibility` needs `ui.visibility` | absent / non-conforming / could-not-judge (served `tools/list` owed) |
141
+ | **C33** identity-tool | C-33 | on an authenticated server, the identity tool: no argument, `readOnlyHint: true`, an `outputSchema` that declares `id` (inline or a same-tree const) | absent / non-conforming / could-not-judge |
142
+ | **S29**..**S34** | S-29..S-34 | the `events` capability and the three `events/*` handler literals; descriptors (`name`, bilingual description, non-empty `delivery`, `inputSchema`, `payloadSchema`); the `-32012`/`-32602`/`whsec_`, `-32015`/`verification`, five-header/`standardwebhooks`/262144-byte/410-413 evidence; `events.accessRecheckSeconds > 0` in the server manifest | absent (no events surface) / non-conforming / could-not-judge (dynamic half named) |
143
+ | **S5** authz-elements (f)-(l) | S-5 (f)..(l) | PRM `resource` and its path-suffixed route, CIMD + `"none"`, `iss` flag, `/authorize` handler (auto-approval, organisation from a query parameter), jwt-bearer, OIDC discovery | absent / non-conforming / could-not-judge (`VERDICT DUE (C-23)` sites exposed; probes owed) |
144
+ | **E7** middleware-events | E-7 | writes to `delivery`/`arguments`/`id`/`refreshBefore`/`cursor` and replacement of `ctx.output` inside an `events/*` branch of a hook | absent / present / non-conforming |
145
+ | **E8** official-ext-declared | E-8 | `capabilities.extensions` as an object, each official key's MUST methods registered | absent / present / non-conforming / could-not-judge (unknown official extension) |
146
+ | **E9** host-invoked-tools | E-9 | `openai/settings.readTool`, `openai/mentions.searchTool`: registered, `readOnlyHint: true`, `outputSchema`, no required input, `"app"` visibility, ResourceLink | absent / present / non-conforming |
147
+ | **E10** mrtr-only | E-10 | per-request transport + `elicitInput` / `elicitation/create` | absent / present / non-conforming |
148
+ | **E11** surfaces-derived | E-11 | `extension.json.surfaces` against `bin/`, `hooks/`, `agents/`, local MCP commands | absent / present / non-conforming |
149
+ | **E12** host-capability-placement | E-12 | host capability under `extensions` rather than only `experimental` (for a 2026-07-28 server) + the E-9 contract | absent / present / non-conforming / could-not-judge |
150
+ | **A8** ui-skills-disjoint | A-8 | `ui://` literals in skill manifests or naming a skill file | absent / non-conforming / could-not-judge |
151
+ | **A9** entrypoint | A-9 | `_meta["openai/ui"].entrypoints`: `{}` input, title, `currentColor` icon, dotted file extensions, `ontoolresult` before `connect` | absent / present / non-conforming |
152
+ | **A10** display-modes | A-10 | `availableDisplayModes` of the resource against the App's, `pip` refused for ChatGPT | absent / present / non-conforming / could-not-judge |
153
+ | **A11** sandbox-domain-derived | A-11 | a `ui://` resource served over HTTP and a SHA-256 + `.claudemcpcontent.com` derivation | absent / non-conforming / could-not-judge |
154
+ | **A12** file-write | A-12 | `openai/resources/write`: `ifMatch`, `conflict` + `too-large` handled, received URI only | absent / present / non-conforming |
155
+
156
+ Amended rules, adapted in place: **C-2** (a tool name is at most 64 characters), **C-15** (a `title`,
157
+ and no enumerated parameter spanning a read and a write verb), **C-16** (event payloads), **S-3**
158
+ (`skills/list` and `skills/get`), **S-4** (the closed trigger: manifest `minProtocolVersion >=
159
+ 2026-07-28`, the `events` capability, a host extension), **E-1** (the vendor-namespace registry
160
+ `vendor-namespaces.json`, `openai/` first member, shipped in the package and read by the detector),
161
+ **E-3** (the fourth type, `host-capability-adapter`), and, with new detectors because they never had
162
+ one, **S-12**, **A-2** and **A-7**. **C-6** now flags a log of an environment variable that names a
163
+ URL / endpoint / host, and of `process.cwd()` and the other absolute-path sources, through
164
+ `console.*`, `logger.*` and `process.stderr|stdout.write`, directly or through an identifier, a
165
+ destructured binding or an object property.
166
+
167
+ The coverage grid (`analysis/mcp-doctor-coverage-grid.md`) is derived by
168
+ `npx tsx scripts/standard-coverage.ts <pin> <ElPi-Corp checkout>`; the foreign-material bite proof is
169
+ `npm run test:v070-foreign-bite` (see `scripts/v070-foreign-bite-probe.sh`).
170
+
128
171
  **DLocalIdentity exemptions.** Two, both reported as `EXEMPT` rather than hidden: the
129
172
  `@vantageos/cloud-identity` package itself (read from the target's `package.json` name), and a site
130
173
  carrying `// allow-local-identity: <reason of 8 characters or more>` on the line, in the comment
package/dist/doctor.d.ts CHANGED
@@ -134,7 +134,7 @@ interface Detector {
134
134
  * so the pre-existing detectors that predate this field still compile; every
135
135
  * detector built or re-pinned against a standard rule MUST populate it.
136
136
  * Coverage of the standard is DERIVED from this field across the registry
137
- * (see `scripts/trunk-coverage.ts`) — never from a hand-kept list.
137
+ * (see `scripts/standard-coverage.ts`) — never from a hand-kept list.
138
138
  */
139
139
  rule?: string;
140
140
  /** The standard commit this detector's `rule` was read against. Required