@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 +201 -0
- package/README.md +65 -3
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +9848 -4733
- package/package.json +5 -7
- package/vendor-namespaces.json +33 -0
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/
|
|
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
|