@vantageos/mcp-doctor 0.5.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,207 @@
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
+
84
+ ## 0.6.0 — 2026-10-10
85
+
86
+ Rebased onto 0.5.0 (S-28, PR #53); this release ships as 0.6.0.
87
+
88
+ Measured on `vantage-mcp-boilerplate` @ `577c6e0b` (`--mode=static`: 14 non-conforming, 3
89
+ could-not-judge under 0.4.0) and against the standard as merged in ElPi-Corp #241 (`572a8835`) +
90
+ fix-forward #242. Every changed reading below has a RED test committed before its fix and a bite
91
+ mutation on foreign material (`scripts/v050-foreign-bite-probe.sh`).
92
+
93
+ ### Fixed (false positives and blind spots)
94
+ - **C15 reads `annotations: TOOL_ANNOTATIONS`.** An `annotations:` value that is not an inline
95
+ object is resolved to a `const X = { ... }` in the same file (or the only such definition in the
96
+ tree; `...SPREAD` entries are followed). Unresolvable (imported from a package, a call, a member
97
+ access) is `could-not-judge` naming the expression, never "missing hint". The same abstention
98
+ now protects C-8, C-9, C-10, C-11 and C-13, which read the same annotations window.
99
+ - **DServer14 Element 1 recognises the delegated challenge.** A call to `unauthorizedChallenge(` or
100
+ `verifyMcpAccessToken(` imported from `@vantageos/cloud-identity` (or its `/resource-server`
101
+ subpath, multi-line and aliased imports included) with the result's `status` and `WWW-Authenticate`
102
+ header forwarded — in the same module or another — is the 401 challenge. A delegation call whose
103
+ forwarding is not readable is `could-not-judge`. The kit's verifier also counts as the RS256/JWKS
104
+ verification (Element 4). A same-named function imported from another package is not a delegation.
105
+ - **DServer14 Element 2 reads a PRM route registered through a constant**
106
+ (`for (const path of PRM_PATHS) app.get(path, ...)`), a blind spot that Element 1 had been masking.
107
+ - **C19 reads the description forms servers actually write:** a `+` concatenation of literals across
108
+ lines, a template literal without expressions, and a string holding the other quote character
109
+ (`"the caller's org"` ended the literal at the apostrophe). A `description:` whose value is not
110
+ readable (a call, an interpolated template) is `could-not-judge` naming it. A tool's window now
111
+ stops at the previous tool's call, so a neighbour's description is never claimed.
112
+ - **D10 reads `description_fr:` with the literal on the next line,** a `+` concatenation, or an
113
+ identifier bound to a same-tree `const X = "..."`. Any other value (`t(locale, ...)`, a call) stays
114
+ `could-not-judge` and the reason now names the form.
115
+ - **The code mask lexes JSX/TSX.** In `.tsx`/`.jsx` files a `<` at an expression position opens an
116
+ element; the text between tags is not code, `/>` is not a regex, an apostrophe in JSX text is not a
117
+ string. `src/ui/entry.tsx` no longer reads "brackets never balance". A file that ends inside an
118
+ unterminated JSX element, template literal or block comment is now unreadable (fail closed) instead
119
+ of having its tail swallowed.
120
+ - **D16 recognises the SDK v2 server construction.** `new McpServer({ name, version })` whose
121
+ `version` is read from `package.json` (directly, or through a `const` bound to a statement that
122
+ reads `package.json`'s `.version`, in any file) is the observable version: the SDK serves it on
123
+ `server/discover` and `initialize`. The verdict is `[static read]` and declares the residual
124
+ (pre-auth reachability of the SDK transport is the dynamic half). A literal version, or an
125
+ identifier bound to anything else, is not credited.
126
+
127
+ ### Fixed (measured on vantage-peers `mcp-server` @ `2f0e5469`, Sigma's triage)
128
+ - **D2 stateless: a header NAME in a CORS list is not session keying.** Every `mcp-session-id` mention
129
+ is classified: a keying form (a `sessionIdGenerator` other than `undefined`, the header read or
130
+ written by call or index, sent as an object key, or a store keyed by a session id) is
131
+ `non-conforming`; a CORS `allowHeaders` / `exposeHeaders` entry is inert and named in the verdict;
132
+ anything else is `could-not-judge` naming the site. The old oracle fixture, which was only CORS
133
+ lists, now carries a real generator.
134
+ - **DServer6 reports every site and resolves literal values.** It returned on its first match; it now
135
+ lists all sites (Forms A, B, C, D). A name match whose value is proven a literal constant
136
+ (`HOSTNAME = "0.0.0.0"` reached through `server.hostname`) is not a finding and is named as not
137
+ counted; an unresolved value is kept and says "identifier-name match, value unresolved". A call is
138
+ never resolved.
139
+ - **C17 reads the repository-root workflows.** For a package scanned as a subdirectory the check
140
+ walks from the scan root up to the git root (the nearest `.git`) and reads that
141
+ `.github/workflows`; it never reads above the git root and never leaves the scanned tree when
142
+ there is no `.git`.
143
+ - **C25 recognises a build identity on `/health`.** A `/health` handler that serves `version` and a
144
+ derived `commit` (or any C25 identity key) beside it is the build identity of the tree served; a
145
+ hand-written literal there is red.
146
+ - **Registration wrappers (`defineTool(...)`).** A function whose body calls `registerTool` / `.tool`
147
+ (directly or through a local alias) with one of its own parameters as the tool name is a
148
+ registration wrapper; the parameter index is derived, and the literal at that index of every call
149
+ is a tool name (a constant bound to a string literal is resolved; anything else is an unresolved
150
+ site, as before). The tool's annotations are read from its call, its description from the argument
151
+ the wrapper forwards, and the wrapper's own internal `registerTool(name, ...)` is no longer
152
+ reported as an unresolved tool. C8-C13, C15, C19 and DServer2 judge those tools instead of
153
+ abstaining.
154
+
155
+ ### Corrected after review (PR #54)
156
+ - **C17 reads a publish step, nothing else.** Provenance is an actual package-manager `publish`
157
+ invocation carrying `--provenance` (YAML/shell comments stripped, `\` continuations joined) or
158
+ `publishConfig.provenance: true`. A comment that names the flag and the `id-token: write`
159
+ permission (necessary for OIDC, attests nothing) are no longer a claim.
160
+ - **C15: a one-line constant closes on its own line.** `const PING_ANN = { title: "x" };` no longer
161
+ reads on into the next constant and borrows its hints. A tool whose own `annotations: X` is an
162
+ expression reads only X; the sibling-attribution fix now has a test that fails without it.
163
+ - **C15 reads spreads in order.** In `annotations: { ...CONST, key: value }` a later member
164
+ overrides the spread (`key: undefined` un-declares a hint, a boolean keeps it); a spread that
165
+ cannot be resolved is `could-not-judge` naming it.
166
+ - **C25 resolves a constant.** `commit: GIT_SHA` with `const GIT_SHA = "deadbeef"` is the same
167
+ hand-written literal and stays red.
168
+
169
+ ### Changed (rule changes from the merged standard)
170
+ - **D7:** conformant when the tree depends on `@modelcontextprotocol/server` >= 2 and no result
171
+ builder is written by hand (`resultType` / `ttlMs` / `cacheScope` spelled in source, outside the
172
+ SDK's `cacheHints` option). The protocol-kit requirement applies only to hand-rolled builders:
173
+ SDK v2 plus a hand-rolled builder and no kit is `non-conforming` at the builder. A dependency range
174
+ that is a tag (`latest`) is `could-not-judge`. SDK v1 / no SDK keeps the 0.4.0 `absent`.
175
+ - **C-17 non-CI path.** A package published without CI has no npm provenance; the standard accepts the
176
+ `npm pack` shasum the reviewer reproduced and read back at the registry. **Proposed evidence form —
177
+ the reviewer rules:** one line `Provenance: <pkg>@<version> npm-pack shasum <40-hex> reproduced by
178
+ <who>, read back from the registry <date>` in `PROVENANCE`, `PROVENANCE.md`, `CHANGELOG.md` or
179
+ `README.md`, naming `<pkg>@<version>` equal to `package.json`'s `name@version`. A record for another version or
180
+ another package, one that names only a version, or one with no 40-hex shasum, is
181
+ `non-conforming` and says why. Without `--provenance` and without a record it is
182
+ still `non-conforming`. Whether the shasum equals the registry's `dist.shasum` stays the gate.
183
+ - **C-18 identifies the server manifest by content.** A root `mcp.json` carrying `mcpServers` is the
184
+ OpenAI plugin config, not the server manifest; the server manifest is the root file (`mcp.json` or
185
+ `mcp-server.json`) carrying capabilities + transport + version. `non-conforming` when no root file
186
+ qualifies (the verdict says why `mcp.json` did not count) and when two root files each qualify.
187
+ - **S-5 package rule (DServer14):** `non-conforming` on a shipped dependency (`dependencies`,
188
+ `peerDependencies`, `optionalDependencies`) on `@vantageos/mcp-auth-kit`, or on
189
+ `@vantageos/cloud-identity` whose lowest admissible version is below 0.15.0; a range whose floor
190
+ cannot be read (`latest`) is `could-not-judge`. The import-subpath half of S-5 is not read.
191
+ - **DLocalIdentity: minting is not resolving the caller.** Sites that sign or mint (`new SignJWT`,
192
+ `jwt.sign`, `importPKCS8`, `createPrivateKey`, `importJWK` of a private key) and the claims written
193
+ into a signing chain's payload are listed under `MINTING/SIGNING` and never counted. Local signature
194
+ verification (`jwtVerify`, `createLocalJWKSet`, ...) and raw claim reads that identify the caller
195
+ stay violations whoever issued the token: there is no issuer carve-out.
196
+
197
+ ### Also
198
+ - **DLocalIdentity exemption marker renamed** `local-identity-exempt:` -> `allow-local-identity:`
199
+ (standard R-57), reason of 8 characters or more (was 10). The old spelling no longer exempts.
200
+ - **DLocalIdentity, third REVISE (PR #52):** reported now are `claimedName` (and any
201
+ `caller*`/`actor*`/`acting*`/`claimed*` property) compared to an agent's `.name`;
202
+ `switch (row.createdBy) { case caller: }`; `[caller].includes(row.createdBy)` and
203
+ `new Set([caller]).has(...)`; `a.localeCompare(b) === 0` on names or tenants; a primitive
204
+ imported or destructured under an alias (`import { createHash as h }`). Implemented as regex
205
+ matchers, not an AST: `typescript` is a devDependency only and the report contract is line-based.
206
+
6
207
  ## 0.5.0 — unreleased
7
208
 
8
209
  ### Added — S-28, a tool forwards the verified claim of its caller (standard `bacc54a`)
package/README.md CHANGED
@@ -119,12 +119,55 @@ Each detector maps one standard item and is proven **bipolar** on real material:
119
119
  | **D4** CacheableResult | `ttlMs` / `cacheScope` on the list/read result types | present / absent |
120
120
  | **D5** server/discover | a `server/discover` handler is registered (absence-based, with a positive control) | present / absent |
121
121
  | **D6** mcp-apps ui-key | MCP Apps UI key is the GA nested `_meta.ui.resourceUri` — not flat `ui/resourceUri`, not community `io.modelcontextprotocol/ui` | GA-nested / flat-deprecated / community-nonGA / none |
122
- | **D7** protocol-kit | `@vantageos/mcp-protocol-kit` listed in `package.json` **and actually called** (builder invoked, not merely declared) | listed+called / listed-not-called / hand-rolled |
122
+ | **D7** protocol-kit | `@vantageos/mcp-protocol-kit` listed in `package.json` **and actually called** (builder invoked, not merely declared) | listed+called / listed-not-called / hand-rolled. Conformant too on `@modelcontextprotocol/server` >= 2 with no hand-rolled result builder (the kit is required only where builders are written by hand) |
123
123
  | **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 |
124
- | **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 |
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. | present (0, or EXEMPT) / non-conforming (count > ceiling, default 0) / could-not-judge |
124
+ | **D16** version-observable | Server Rule 16 — `server/discover` (or an HTTP `/health` route) exposes a `version` field sourced from `package.json`, reachable pre-auth; an SDK v2 `new McpServer({ name, version })` carrying a `package.json`-derived version counts (pre-auth reach is the dynamic half) | present / absent / non-conforming / could-not-judge |
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
@@ -134,6 +177,25 @@ not UTF-8 text, is `could-not-judge`. To adopt the rule on a tree that still hol
134
177
  `--local-identity-ceiling=<current count>` as a ratchet; the row then reads `present` and still lists
135
178
  every site.
136
179
 
180
+ **Reading rules, 0.6.0** (each changed detector; details and rationale in [CHANGELOG.md](./CHANGELOG.md)).
181
+ - **C15 / C8 / C9 / C10 / C11 / C13** resolve `annotations: SOME_CONST` to a same-tree `const` object; an
182
+ unresolvable value is `could-not-judge`, never "missing".
183
+ - **C19** counts `"a" + "b"`, an expression-free template literal and apostrophe-bearing strings as a
184
+ declared description; **D10** reads `description_fr:` with the literal on the next line or a const.
185
+ - **DServer14** accepts a 401 challenge delegated to `@vantageos/cloud-identity`
186
+ (`unauthorizedChallenge` / `verifyMcpAccessToken`, status + header forwarded), a PRM route registered
187
+ through a path constant, and is red on `@vantageos/mcp-auth-kit` or `cloud-identity` below 0.15.0.
188
+ - **C17** accepts, without CI, a `Provenance: <pkg>@<version> npm-pack shasum <40-hex> ...` record naming the
189
+ published version (**proposed evidence form, reviewer rules**); **C18** identifies the server manifest by
190
+ content (`mcpServers` = OpenAI plugin config; the manifest is `mcp-server.json` then) and is red on two.
191
+ - **D2** names `mcp-session-id` in a CORS list as inert and flags only real session keying;
192
+ **DServer6** reports every site and does not count a name whose value is a literal constant;
193
+ **C17** reads the repository-root `.github/workflows` for a subpackage; **C25** accepts a derived
194
+ `commit` served on `/health` beside `version`; a project-local registration wrapper
195
+ (`defineTool(...)`) is derived from the function that forwards a parameter to `registerTool`, so its
196
+ tools are judged.
197
+ - **TSX/JSX files** are lexed; a file that ends inside an unterminated JSX element is unreadable, not skipped.
198
+
137
199
  **This table is a sample, not the inventory.** Every registered detector runs on every invocation,
138
200
  so the authoritative list is simply the output of a run — one row per detector. The registered set
139
201
  lives in `src/detectors/index.ts`. No count is written here on purpose: a number typed into a README
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