@vantageos/mcp-doctor 0.6.0 → 0.8.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 +210 -0
- package/README.md +106 -1
- package/dist/doctor.d.ts +56 -2
- package/dist/doctor.js +12264 -5377
- package/package.json +5 -2
- package/vendor-namespaces.json +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,216 @@
|
|
|
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.8.0 — 2026-10-11
|
|
7
|
+
|
|
8
|
+
Measured against the same standard pin as 0.7.0, `b714129a110237c5521d5269ee61ab919408a17f` (91
|
|
9
|
+
rules). 0.7.0 covered 64 of them; 0.8.0 adds a detector for each of the 27 left, so the registry
|
|
10
|
+
covers all of them:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npx tsx scripts/standard-coverage.ts b714129a110237c5521d5269ee61ab919408a17f ../elpi-corp-std
|
|
14
|
+
-> 91/91 rules covered by a registered detector (pin b714129a110237c5521d5269ee61ab919408a17f)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The 27 are derived from that script's `UNCOVERED` rows at 0.7.0, not listed from memory: **C-23 C-24
|
|
18
|
+
C-27 C-28 · S-8 S-9 S-10 S-11 S-13 S-14 S-15 S-16 S-17 S-18 S-19 S-20 S-21 S-22 S-23 S-24 S-25 S-26
|
|
19
|
+
S-27 · E-5 · A-3 A-4 A-5**. Every one has a RED test committed before its implementation and a bite
|
|
20
|
+
mutation on foreign material, one per form of the guarded thing, with its legitimate pole
|
|
21
|
+
(`scripts/v080-foreign-bite-probe.sh`, `npm run test:v080-foreign-bite`).
|
|
22
|
+
|
|
23
|
+
### Added — one detector per uncovered rule
|
|
24
|
+
- **Trunk gates, static half read, gate half named as owed:** **C23** judgement-sites, **C24**
|
|
25
|
+
acceptance-at-dispatch, **C27** subpackage-types-in-isolation, **C28** deploy-failure-signal.
|
|
26
|
+
- **Server:** **S8** token-passthrough, **S9** confused-deputy-proxy, **S10** ssrf-guard, **S11**
|
|
27
|
+
result-contract, **S13** progress-cancel, **S14** version-negotiation, **S15** completion-security,
|
|
28
|
+
**S16** local-footprint, **S17** forward-auth, **S18** session-integrity, **S19** auth-url-open,
|
|
29
|
+
**S20** scope-minimization, **S21** stdio-proxy-sandbox, **S22** origin-validation, **S23**
|
|
30
|
+
protocol-version-header, **S24** tool-rate-limit, **S25** resource-discipline, **S26**
|
|
31
|
+
list-changed-parity, **S27** elicitation-floor.
|
|
32
|
+
- **Extension:** **E5** extension-isolation. **App:** **A3** sandbox-respected, **A4** wcag-axe-gate,
|
|
33
|
+
**A5** performance-budgets.
|
|
34
|
+
- A dynamique or gate row keeps the 0.7.0 semantics: red where the tree contradicts the rule, `absent`
|
|
35
|
+
only when the subject is provably absent (each matcher carries a positive control and a failed
|
|
36
|
+
control throws), otherwise `could-not-judge` naming the half that is owed. Nothing that has an unread
|
|
37
|
+
half returns `present`.
|
|
38
|
+
- Per-detector reading, limits and verdicts: README, "Detectors added in 0.8.0".
|
|
39
|
+
|
|
40
|
+
### Changed — Argus rulings on 0.7.0 (PR #55)
|
|
41
|
+
1. **An owed human verdict is a structured field, not a state and not a prefix.** There is no fifth
|
|
42
|
+
state. A `could-not-judge` result may carry `owedVerdict: { kind: "human", rule: "C-23", sites:
|
|
43
|
+
[{ anchor, element?, what }] }`; the runner refuses it on any other state or with zero sites. S-5
|
|
44
|
+
(i)/(j) use it in place of the `VERDICT DUE (C-23)` prefix (removed from the prose), and so do C-24,
|
|
45
|
+
C-28 and S-20. The human report prints an "owed human verdicts (N sites)" section; the new `--json`
|
|
46
|
+
flag prints `{ surface, exitCode, owedHumanVerdicts, results, skipped }` with the field in each
|
|
47
|
+
result.
|
|
48
|
+
2. **S-5 (g) and (l) match a quoted key.** `"client_id_metadata_document_supported": true` (and the
|
|
49
|
+
single-quoted spelling, and the same for `authorization_response_iss_parameter_supported`) was a
|
|
50
|
+
false red; each spelling x each key x `true` / `false` is now a test.
|
|
51
|
+
3. **A-11 recognises every literal form of the sandbox domain.** Any quoted `*.claudemcpcontent.com`
|
|
52
|
+
with a label in front of the suffix is a typed literal: any label (hex or not, any case, hyphens,
|
|
53
|
+
several labels), any quoting (double, single, a template with no interpolation), a scheme prefix,
|
|
54
|
+
and a literal joined to the suffix (`"vp" + ".claudemcpcontent.com"`). The bare suffix constant and
|
|
55
|
+
an interpolated label are not literals. One test per form.
|
|
56
|
+
|
|
57
|
+
### Fixed — found by running the new detectors on foreign material
|
|
58
|
+
Each was a false positive or a false green found on `vantage-mcp-boilerplate` or `vantage-peers`,
|
|
59
|
+
turned into a RED test before the fix:
|
|
60
|
+
- **C-28**: a deploy named in a YAML comment is not a deploy step.
|
|
61
|
+
- **C-27**: a workflow is read for the commands its `run:` keys run (inline and block), not for every
|
|
62
|
+
line holding the word; an `echo`, a job name or a comment is not a type check or an install.
|
|
63
|
+
- **S-8, S-24**: a marker word inside a string (an error message, a description) is prose, not an
|
|
64
|
+
audience check or a rate guard; a quoted property key still counts.
|
|
65
|
+
- **S-10**: a blocklist written as CIDR pairs (`["10.0.0.0", 8]`, what `node:net` BlockList takes) states
|
|
66
|
+
the union; the two metadata hostnames are satisfied by name resolution; a host classification
|
|
67
|
+
(`startsWith("127.")` choosing http or https) is not address validation outside a file that reaches
|
|
68
|
+
out.
|
|
69
|
+
- **S-13**: a TypeScript method signature named `fetch(request: Request, ...)` is not an outbound call.
|
|
70
|
+
- **S-17**: `iss` is validated in the file that redeems the code, not anywhere in the tree.
|
|
71
|
+
- **S-22**: a quoted `"0.0.0.0"` in a blocklist constant is not a bind; CORS `origin: "*"` is not an
|
|
72
|
+
Origin validation.
|
|
73
|
+
- **S-25**: an interpolated `${proto}://${host}` is a computed URL, not a custom scheme.
|
|
74
|
+
- **A-3, A-4**: a tree that serves `ui://` resources and holds no UI code is `could-not-judge`, not a
|
|
75
|
+
verdict on code that is not there.
|
|
76
|
+
|
|
77
|
+
### Revised after review (PR #56, Argus REVISE at `b60df486`)
|
|
78
|
+
**The exit-0 pole is reachable.** `computeExitCode` returned 2 on any `could-not-judge`, and six new rules
|
|
79
|
+
had no way to say `present`, so no server that registered a tool could be approved. Each rule now declares
|
|
80
|
+
its choice, in its detector header and here:
|
|
81
|
+
- **S-11, S-13, S-16, A-4, A-5, A-3 — choice (b), a static positive path on positive evidence.** `present`
|
|
82
|
+
names the half it read (`static half: ...`) and the half it did NOT read (`NOT read by this run: ...`);
|
|
83
|
+
nothing is claimed about the unread half.
|
|
84
|
+
- S-11: every registered tool declares an `outputSchema` and no error code contradicts; a tool with none
|
|
85
|
+
whose window shows no result shape is `could-not-judge`, named.
|
|
86
|
+
- S-13: every outbound call carries a timeout (or none is emitted), no stale id, no answering cancel handler.
|
|
87
|
+
- S-16: no read outside the object, and a transport posture is identified (stdio, HTTP with a token check, a
|
|
88
|
+
Unix socket); tools with no identifiable transport are `could-not-judge`.
|
|
89
|
+
- A-4: an axe dependency and something that runs it; the CI result stays the gate's.
|
|
90
|
+
- A-5: the measured gzipped size is under 250 KB; with no built bundle in the tree it is `could-not-judge`.
|
|
91
|
+
- A-3: UI code, HTML shells and HTML held in a string literal (`ui://` served from `text: \`<html>\``) are
|
|
92
|
+
read; `ui://` served with no UI code anywhere stays `could-not-judge` (the code is elsewhere).
|
|
93
|
+
- S-14 (not in the review's list, same defect, found by the green-pole test): an SDK-first server whose
|
|
94
|
+
tree shows the official SDK (import or dependency) is `present` on its static half; a server class with
|
|
95
|
+
no SDK evidence stays `could-not-judge`.
|
|
96
|
+
- **S-20 — choice (c), an owed human verdict.** A result whose only open question is the human verdict on the
|
|
97
|
+
exposed sites declares `owedVerdict.mechanicalHalfComplete: true`. **Exit-code treatment, defined and
|
|
98
|
+
tested (`tests/v080-exit-code-owed.test.ts`):** such a result is OWED, not unreadable. It does not raise
|
|
99
|
+
exit 2 (exit 0 when nothing else does) and its sites stay printed and counted in `owedHumanVerdicts`. A
|
|
100
|
+
`could-not-judge` with no such declaration (S-5, whose probes are also owed) exits 2 as before, and a
|
|
101
|
+
violation still exits 1. Not defined: S-23 and S-10 on an SDK HTTP server still end in `could-not-judge`
|
|
102
|
+
(the SDK version that validates the header is not readable from the tree) — they hold exit 2 for such a tree.
|
|
103
|
+
- `reconcile()` (the `--mode=both` divergence) now carries the owed sites of either side.
|
|
104
|
+
- `fixtures/oracle-green-conforming` is repaired to conform to the 0.8.0 rules it failed: a real token-bucket
|
|
105
|
+
rate guard on the tool path (S-24), a stdio transport, a bridge-only UI shell, an axe gate, a built bundle.
|
|
106
|
+
A test asserts it exits 0 statically with S-11/13/16/24 and A-3/4/5 `present`, and exits 1 once the guard
|
|
107
|
+
is removed.
|
|
108
|
+
|
|
109
|
+
**Two security detectors no longer fail open.**
|
|
110
|
+
- **S-8** read the incoming Authorization header only through `.get("authorization")` or a quoted bracket, so
|
|
111
|
+
`authorization: c.req.raw.headers.authorization` forwarded to `fetch` read `present`. The census of reads
|
|
112
|
+
(a call, a bracket, DOT access with or without optional chaining, a destructuring renamed or not, any case)
|
|
113
|
+
is one test per form.
|
|
114
|
+
- **S-22** accepted a bare `allowedOrigins:` as protection. It is now read by VALUE: a wildcard (bare, in a
|
|
115
|
+
list, as a host pattern) and an empty list are red; a list of concrete origins is `present`; a variable is
|
|
116
|
+
resolved in the tree (wildcard / empty / concrete) or reported `could-not-judge` naming it; the protection
|
|
117
|
+
flag with no list is red. One test per shape.
|
|
118
|
+
|
|
119
|
+
**Sweep.**
|
|
120
|
+
- Source the scan does not read (`.js .mjs .cjs .jsx .mts .cts`; declarations, tests, config and
|
|
121
|
+
`_generated/` excluded): the code-reading rules (C-23, S-8..S-27, E-5, A-3) say `could-not-judge` naming the
|
|
122
|
+
files instead of `absent`. `src/bin/**` is product source (only a top-level `bin/` is tooling), and tools
|
|
123
|
+
registered by tooling are not the server's.
|
|
124
|
+
- `present` on a word: S-9 (needs a consent lookup, an equality test on `redirect_uri`, a random state), S-20
|
|
125
|
+
(a bare challenge declares no scope), S-24 (a rate-limit identifier must be CALLED), S-17 (the issuer is a
|
|
126
|
+
word of the store key, not the letters of `permissionId`), S-16 (a `footprint` KEY with content, not the
|
|
127
|
+
word), E-5 (zero files read is `could-not-judge`).
|
|
128
|
+
- Comment text as evidence: C-23 (a genre is declared by a code literal or a comment opening with `genre:`),
|
|
129
|
+
A-4 (workflow and test-file comments are not a run).
|
|
130
|
+
- Silent skips: S-15 (case-clause handlers are read; unreadable sites are named), A-5 (oversize, dangling or
|
|
131
|
+
unreadable UI paths are `could-not-judge`, named), C-27 (an unreadable deployable directory is named).
|
|
132
|
+
|
|
133
|
+
### Process
|
|
134
|
+
- `tests/__oracle__/oracle-complete-server.static.json` is regenerated: 27 rows added, no existing row
|
|
135
|
+
changed.
|
|
136
|
+
- Version `0.8.0` is strictly above the live `latest` (`0.7.0`), read from the registry.
|
|
137
|
+
|
|
138
|
+
## 0.7.0 — 2026-10-10
|
|
139
|
+
|
|
140
|
+
Measured against the ElPi-Corp standard pinned at `b714129a110237c5521d5269ee61ab919408a17f`
|
|
141
|
+
(91 rules; ElPi-Corp #243 on top of `2c4dedbe`). Every new or changed detector has a RED test
|
|
142
|
+
committed before its implementation, a bite mutation on foreign material and the legitimate form of
|
|
143
|
+
the same edit (`scripts/v070-foreign-bite-probe.sh`), and its restore proven (`diff -r` empty).
|
|
144
|
+
|
|
145
|
+
New rule ids, derived (`git diff 2c4dedbe b714129a` over the four standards, ids of the numbered
|
|
146
|
+
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 ·
|
|
147
|
+
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,
|
|
148
|
+
C-6 (debt), C-15, C-16, S-3, S-4, S-10, S-12, E-1, E-3, A-2, A-7.
|
|
149
|
+
|
|
150
|
+
### Added — one detector per new rule
|
|
151
|
+
- **C29 served-skills**, **C30 skills-org-bound**, **C31 plugin-installable**, **C32
|
|
152
|
+
host-key-additive**, **C33 identity-tool**.
|
|
153
|
+
- **S29 events-declared**, **S30 event-descriptors**, **S31 subscription-contract**, **S32
|
|
154
|
+
callback-verification**, **S33 signed-delivery**, **S34 access-recheck**.
|
|
155
|
+
- **S5 authz-elements (f)-(l)** — PRM `resource` and suffixed route, CIMD + `"none"`, `iss`,
|
|
156
|
+
`/authorize` authenticating a person, jwt-bearer, OIDC discovery.
|
|
157
|
+
- **E7** middleware-events, **E8** official-ext-declared, **E9** host-invoked-tools, **E10**
|
|
158
|
+
mrtr-only, **E11** surfaces-derived, **E12** host-capability-placement.
|
|
159
|
+
- **A8** ui-skills-disjoint, **A9** entrypoint, **A10** display-modes, **A11**
|
|
160
|
+
sandbox-domain-derived, **A12** file-write.
|
|
161
|
+
- **What a dynamique row returns on a static run.** `absent` only when the subject is statically
|
|
162
|
+
proven absent (the matcher carries a positive control proving it can see the declaration); a red
|
|
163
|
+
where the tree contradicts the rule; otherwise `could-not-judge` naming the half that is owed —
|
|
164
|
+
never `present`, and never `absent` for a surface it found. The dynamic halves (a served
|
|
165
|
+
`tools/list`, two scoped identities, a webhook receiver) are not implemented; each detector's
|
|
166
|
+
header and verdict name what is owed.
|
|
167
|
+
- **`VERDICT DUE (C-23)`**: S-5 (i) and (j) are partly judgement. The detector exposes the sites and
|
|
168
|
+
returns `could-not-judge` with that prefix; the four states cannot say "sites exposed, verdict due",
|
|
169
|
+
and C-23 forbids `present` and `absent` there. Reported as a missing fifth state.
|
|
170
|
+
|
|
171
|
+
### Added — amended rules, adapted in place
|
|
172
|
+
- **C-2**: a tool name over 64 characters is red (static and runtime names).
|
|
173
|
+
- **C-15**: a tool without a `title` (its own field, else `annotations.title`) is red; an enumerated
|
|
174
|
+
parameter holding a read verb and a write verb of the closed set (§3) is red.
|
|
175
|
+
- **C-16**: a `payloadSchema` string field named as a free body next to a read tool is red.
|
|
176
|
+
- **S-3**: when Skills is declared, `skills/list` and `skills/get` must carry `ttlMs`/`cacheScope`.
|
|
177
|
+
- **S-4**: `server/discover` is required on the closed trigger (manifest `minProtocolVersion >=
|
|
178
|
+
2026-07-28`, the `events` capability, a host extension); with none of them the rule does not oblige.
|
|
179
|
+
An SDK v2 dependency (it serves `server/discover`) makes a missing literal `could-not-judge`.
|
|
180
|
+
- **E-1** and **`vendor-namespaces.json`** at the repository root (`openai/` first member, plus the
|
|
181
|
+
keys owned by another rule), shipped in `files` and read at the package's own version; an
|
|
182
|
+
unreadable registry is `could-not-judge`. A vendor key under `_meta`, `capabilities.extensions` or
|
|
183
|
+
`capabilities.experimental` is admitted only if its namespace is a member; an extension identifier
|
|
184
|
+
under a vendor namespace is red (we never publish under one).
|
|
185
|
+
- **E-3**: the fourth type, `host-capability-adapter`.
|
|
186
|
+
- **S-12**, **A-2**, **A-7** had no detector; they have one now (S-10 still has none: its only new
|
|
187
|
+
reading is a dynamic loopback receiver).
|
|
188
|
+
- **C-6 debt (named by the reviewer on #54)**: a log of an environment variable naming a URL /
|
|
189
|
+
endpoint / host, or of `process.cwd()`, is Form D. The class is swept: `os.homedir()`,
|
|
190
|
+
`os.tmpdir()`, `process.execPath`, `import.meta.dirname|filename|url`; `process.stderr|stdout.write`
|
|
191
|
+
counts as a log call; the value is followed through an identifier, a destructured binding and an
|
|
192
|
+
object property. A non-URL variable (`NODE_ENV`) and a read that is not logged stay green.
|
|
193
|
+
|
|
194
|
+
### Changed
|
|
195
|
+
- Every detector now carries `rule` + `pin` = the standard pin above (the detectors that predate the
|
|
196
|
+
restructure were mapped to their current ids: D2/D12 S-1, D3 S-2, D15 S-6, D16 S-7, D6 A-6,
|
|
197
|
+
DServer8 A-1, DServer14 and DLocalIdentity S-5, DExt5 C-1, DExt7 C-5, DExt8 C-3, DExt9 E-6).
|
|
198
|
+
Re-pinning is not a retype: for each, the rule text is byte-identical between the detector's old
|
|
199
|
+
pin and the new one (C-17, C-18, C-25, C-26, S-28: identical between `2c4dedbe` and `b714129a`).
|
|
200
|
+
- `scripts/trunk-coverage.ts` is replaced by `scripts/standard-coverage.ts` (four standards, genre
|
|
201
|
+
column read from the detector table). `analysis/mcp-doctor-coverage-grid.md` is regenerated from it.
|
|
202
|
+
- Fixtures updated for rules that now require more: a `title` on every tool of the green C-15
|
|
203
|
+
fixtures; the oracle fixtures carry a plugin folder (C-31 is `could-not-judge` without one).
|
|
204
|
+
- A tool-method literal that is an object KEY (a cache-hints table) is not a handler.
|
|
205
|
+
|
|
206
|
+
### Fixed (found by the foreign bite probe)
|
|
207
|
+
- A-2 credited a single-file plugin that was only NAMED in a comment; comments are now ignored.
|
|
208
|
+
- S-5 (f) followed only the first `resource:` near `authorization_servers`; every PRM site is judged
|
|
209
|
+
and a member read (`s.resource`) is followed to its definitions.
|
|
210
|
+
|
|
211
|
+
### Measured (`--mode=static`, doctor 0.6.0 -> 0.7.0)
|
|
212
|
+
- `vantage-mcp-boilerplate` @ `cdb14add`: 10 -> 11 non-conforming, 2 -> 6 could-not-judge.
|
|
213
|
+
- `vantage-peers` `mcp-server` @ `9682a0d8`: 11 -> 14 non-conforming, 3 -> 7 could-not-judge.
|
|
214
|
+
- Every new non-conforming / could-not-judge line is named and classified in the pull request.
|
|
215
|
+
|
|
6
216
|
## 0.6.0 — 2026-10-10
|
|
7
217
|
|
|
8
218
|
Rebased onto 0.5.0 (S-28, PR #53); this release ships as 0.6.0.
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ The path is the root of the MCP server sources (the folder holding `server.ts` /
|
|
|
46
46
|
The binary's own `--help` is the authority; this is what it prints:
|
|
47
47
|
|
|
48
48
|
```
|
|
49
|
-
usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>]
|
|
49
|
+
usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<convex-dir>] [--json]
|
|
50
50
|
|
|
51
51
|
--mode=static read the tree only (default)
|
|
52
52
|
--mode=dynamic launch the server and judge what it serves
|
|
@@ -56,6 +56,7 @@ usage: mcp-doctor <source-tree-path> [--mode=static|dynamic|both] [--backend=<co
|
|
|
56
56
|
fails (default 0)
|
|
57
57
|
--backend=DIR the backend source (a convex/ dir) whose door list S-28 reads;
|
|
58
58
|
default: the nearest convex/ above the tree
|
|
59
|
+
--json print the report as JSON (results, owedHumanVerdicts, exitCode)
|
|
59
60
|
--version print the version and exit
|
|
60
61
|
--help print this and exit
|
|
61
62
|
|
|
@@ -77,6 +78,11 @@ The exit code keeps three outcomes distinct, which is what makes the tool safe i
|
|
|
77
78
|
- **1** — at least one violation.
|
|
78
79
|
- **2** — no violation, but at least one detector abstained. **An abstention is never a pass**: it
|
|
79
80
|
means part of the tree was not judged, and the row names which part and why.
|
|
81
|
+
**Exception, defined in 0.8.0:** a `could-not-judge` that carries an `owedVerdict` declaring
|
|
82
|
+
`mechanicalHalfComplete: true` is an OWED human verdict, not an abstention: the detector ran its whole
|
|
83
|
+
mechanical half and exposed the sites a person must decide (C-23: "green is: the sites are exposed"). It does
|
|
84
|
+
not raise exit 2; the sites stay printed and counted (`owedHumanVerdicts`). Any other `could-not-judge`, with
|
|
85
|
+
or without an `owedVerdict`, still exits 2.
|
|
80
86
|
|
|
81
87
|
**One known limit in 0.3.2, stated rather than hidden:**
|
|
82
88
|
|
|
@@ -125,6 +131,105 @@ Each detector maps one standard item and is proven **bipolar** on real material:
|
|
|
125
131
|
| **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
132
|
| **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
133
|
|
|
134
|
+
### Detectors added in 0.7.0 (standard `b714129a`, ElPi-Corp #243)
|
|
135
|
+
|
|
136
|
+
One detector per rule that #243 added, plus the S-5 elements (f)..(l), each implementing the
|
|
137
|
+
detector row of its rule. "Static run" is what the default mode returns; a dynamique row never
|
|
138
|
+
returns `present` statically — it returns `absent` only when its subject is proven absent, a red
|
|
139
|
+
where the tree contradicts the rule, and otherwise `could-not-judge` naming the half that is owed.
|
|
140
|
+
|
|
141
|
+
| ID | Rule | The static run reads | Verdicts |
|
|
142
|
+
|----|------|----------------------|----------|
|
|
143
|
+
| **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 |
|
|
144
|
+
| **C30** skills-org-bound | C-30 | whether Skills is declared | absent / could-not-judge (two scoped, non-master identities owed) |
|
|
145
|
+
| **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) |
|
|
146
|
+
| **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) |
|
|
147
|
+
| **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 |
|
|
148
|
+
| **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) |
|
|
149
|
+
| **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 (sites exposed in `owedVerdict`; probes owed) |
|
|
150
|
+
| **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 |
|
|
151
|
+
| **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) |
|
|
152
|
+
| **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 |
|
|
153
|
+
| **E10** mrtr-only | E-10 | per-request transport + `elicitInput` / `elicitation/create` | absent / present / non-conforming |
|
|
154
|
+
| **E11** surfaces-derived | E-11 | `extension.json.surfaces` against `bin/`, `hooks/`, `agents/`, local MCP commands | absent / present / non-conforming |
|
|
155
|
+
| **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 |
|
|
156
|
+
| **A8** ui-skills-disjoint | A-8 | `ui://` literals in skill manifests or naming a skill file | absent / non-conforming / could-not-judge |
|
|
157
|
+
| **A9** entrypoint | A-9 | `_meta["openai/ui"].entrypoints`: `{}` input, title, `currentColor` icon, dotted file extensions, `ontoolresult` before `connect` | absent / present / non-conforming |
|
|
158
|
+
| **A10** display-modes | A-10 | `availableDisplayModes` of the resource against the App's, `pip` refused for ChatGPT | absent / present / non-conforming / could-not-judge |
|
|
159
|
+
| **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 |
|
|
160
|
+
| **A12** file-write | A-12 | `openai/resources/write`: `ifMatch`, `conflict` + `too-large` handled, received URI only | absent / present / non-conforming |
|
|
161
|
+
|
|
162
|
+
### Detectors added in 0.8.0 (standard `b714129a`) — the 27 rules 0.7.0 left uncovered
|
|
163
|
+
|
|
164
|
+
One detector per remaining rule, so `scripts/standard-coverage.ts` derives **91/91**. Each keeps the
|
|
165
|
+
semantics of a dynamique row: red where the tree contradicts the rule, `absent` only when the subject
|
|
166
|
+
is provably absent (a positive control proves the matcher can see it), otherwise `could-not-judge`
|
|
167
|
+
naming the half that is owed. Comments never count; where a marker word could be prose (a description,
|
|
168
|
+
an error message) it counts only as code or as a quoted property key.
|
|
169
|
+
|
|
170
|
+
| ID | Rule | The static run reads | Verdicts |
|
|
171
|
+
|----|------|----------------------|----------|
|
|
172
|
+
| **C23** judgement-sites | C-23 | every control that declares a judgement genre (the declared column, split on `+`, is the authority): what it returns, whether the due sites travel in `owedVerdict`, a positive control beside any zero | absent / present (sites exposed) / non-conforming |
|
|
173
|
+
| **C24** acceptance-at-dispatch | C-24 | dispatch documents (`tasks/`, `dispatch/`, `briefs/`, issue templates) that build, extend or run a detector / conformance instrument, and the acceptance-property block in them (EN or FR) | absent / non-conforming / could-not-judge (the verdict-side equality is the owed gate; `owedVerdict`) |
|
|
174
|
+
| **C27** subpackage-types-in-isolation | C-27 | each deployable sub-package with a `tsconfig` (C-26's own derivation): a type check that runs after a FROZEN install, read from the Dockerfile `RUN` lines, the railway `buildCommand` and the CI `run:` commands | absent / non-conforming (none / before any install / after a non-frozen install) / could-not-judge (the run result is the owed gate) |
|
|
175
|
+
| **C28** deploy-failure-signal | C-28 | CI deploy steps (railway, vercel, fly, wrangler, netlify, serverless, gcloud run, kubectl, helm); an `if: failure()` step that notifies, or a served-identity-equals-commit check | absent / non-conforming / could-not-judge (a platform-managed deploy, or a signal present; that a person received it is the owed human verdict, `owedVerdict`) |
|
|
176
|
+
| **S8** token-passthrough | S-8 | token verifiers (audience in the same file for `jwtVerify` / `jwt.verify`, anywhere for a library verifier) and any outbound `Authorization` built from the caller's own header, `authInfo.token`, or a variable assigned from them | absent / present / non-conforming / could-not-judge |
|
|
177
|
+
| **S9** confused-deputy-proxy | S-9 | a proxy file (`/authorize` route, `redirect(`, an upstream): consent registry, `redirect_uri` compared exactly (not by `startsWith` / `includes` / `endsWith` / `match` / a regular expression), `__Host-` cookies, `state` after the consent check | absent / present / non-conforming |
|
|
178
|
+
| **S10** ssrf-guard | S-10 | outbound calls whose URL is a caller member (`args.url`, `delivery.url` ...), directly, aliased or destructured; hand-written address tests; `redirect: "follow"`; a blocklist that states at least three members of the union but not all (CIDR pairs count; the metadata hostnames are satisfied by name resolution) | absent / non-conforming / could-not-judge (the loopback-callback and 302 probes are owed) |
|
|
179
|
+
| **S11** result-contract | S-11 | tools returning `structuredContent` without an `outputSchema`; error codes in the reserved range -32020..-32099; a string `code` in a JSON-RPC error | absent / non-conforming / could-not-judge (served conformance owed) |
|
|
180
|
+
| **S13** progress-cancel | S-13 | outbound calls without a `signal` / `timeout`; a literal `requestId` / `progressToken` in an emitted notification; a cancellation handler that answers | absent / non-conforming / could-not-judge (progress monotony, late cancel, `initialize` owed) |
|
|
181
|
+
| **S14** version-negotiation | S-14 | a hand-rolled `initialize` answering a fixed `protocolVersion` | absent / non-conforming / could-not-judge (the negotiation probe is owed) |
|
|
182
|
+
| **S15** completion-security | S-15 | the `completions` capability, `completion/complete`, `completable(`: input validation and a rate limit in the handler | absent / non-conforming / could-not-judge (no-disclosure owed) |
|
|
183
|
+
| **S16** local-footprint | S-16 | reads of `~/.ssh`, cloud credentials, shell history; home-directory reads with no `footprint` in the manifest; HTTP bound to loopback with no token and no socket | absent / non-conforming / could-not-judge (the runtime file trace is owed) |
|
|
184
|
+
| **S17** forward-auth | S-17 | an authorization code redeemed without `iss` handled in the redeeming file; credentials keyed without the issuer; dynamic registration with no CIMD | absent / present / non-conforming |
|
|
185
|
+
| **S18** session-integrity | S-18 | `sessionIdGenerator`: predictable, not a CSPRNG, not bound `<user>:<id>`; a session lookup used as authentication | absent / present / non-conforming |
|
|
186
|
+
| **S19** auth-url-open | S-19 | URL opening through a shell, scheme validation by blocklist, no http(s) allowlist | absent / present / non-conforming |
|
|
187
|
+
| **S20** scope-minimization | S-20 | omnibus scopes, the whole catalogue in `scopes_supported`; the sites that check a CLAIMED scope | absent / present / non-conforming / could-not-judge (`owedVerdict`: does authorization rest on the claim alone) |
|
|
188
|
+
| **S21** stdio-proxy-sandbox | S-21 | child-process launches: a sandbox marker, a log within eight lines, an authorization word for a dangerous command | absent / present / non-conforming |
|
|
189
|
+
| **S22** origin-validation | S-22 | a Streamable HTTP transport: the SDK's DNS rebinding protection, or an `Origin` read followed by a 403; `0.0.0.0` named | absent / present / non-conforming |
|
|
190
|
+
| **S23** protocol-version-header | S-23 | the `MCP-Protocol-Version` read (any case) followed by a 400; a hand-rolled route that never reads it | absent / present / non-conforming / could-not-judge (the SDK owns the check; a probe is owed) |
|
|
191
|
+
| **S24** tool-rate-limit | S-24 | each registered tool's window for a rate-limit word as code (a quoted key counts), or a central `CallToolRequestSchema` handler that limits; each unguarded tool is named | absent / present / non-conforming |
|
|
192
|
+
| **S25** resource-discipline | S-25 | unvalidated dereference of a caller-derived value in a resource handler, a custom scheme that is not RFC 3986, `blob:` that is not base64, binary content without `mimeType` | absent / present / non-conforming |
|
|
193
|
+
| **S26** list-changed-parity | S-26 | `resources` / `prompts` declaring `listChanged: true` against the matching `list_changed` emission, per kind | absent / present / non-conforming |
|
|
194
|
+
| **S27** elicitation-floor | S-27 | sensitive property keys of a `requestedSchema`; MRTR `input_required` with no elicitation fallback and no floor declaration (`midCallInputAtFloor: false`) | absent / non-conforming / could-not-judge (the floor probe is owed) |
|
|
195
|
+
| **E5** extension-isolation | E-5 | global writes (`globalThis`, `process.env`, built-in prototypes), writes to an `identifier`, writes into the shared `extensions` map under a key that is not the extension's own | absent / present / non-conforming |
|
|
196
|
+
| **A3** sandbox-respected | A-3 | `window.parent`, `window.top`, `localStorage`, `sessionStorage`, `document.cookie` in UI code and in the `<script>` blocks of HTML shells | absent / present / non-conforming / could-not-judge (ui:// served, no UI code in the tree) |
|
|
197
|
+
| **A4** wcag-axe-gate | A-4 | an axe dependency and a script, workflow or test that runs it | absent / non-conforming / could-not-judge (0 blocking violations is the owed gate) |
|
|
198
|
+
| **A5** performance-budgets | A-5 | gzipped size of the UI bundle files (`dist/` and `build/` read for size only): one file or one directory over 250 KB | absent / non-conforming / could-not-judge (render, roundtrip, memory probes owed) |
|
|
199
|
+
|
|
200
|
+
**What `present` means for a rule whose other half is dynamic (0.8.0).** S-11, S-13, S-14, S-16, A-3, A-4
|
|
201
|
+
and A-5 return `present` on a static positive path: the verdict says `static half: ...` and names the half it
|
|
202
|
+
did NOT read (`NOT read by this run: ...`) — the served output conforms, progress is monotone, the negotiation
|
|
203
|
+
probe, the runtime file trace, the CI result of the axe run, render/roundtrip/memory. `--mode=dynamic` does not
|
|
204
|
+
implement those reads. Read `present` on these rows as "the static half holds", never as "the rule is held".
|
|
205
|
+
|
|
206
|
+
**An owed human verdict is a field, not a state.** There is no fifth state. A `could-not-judge` whose
|
|
207
|
+
owed half is judgement carries `owedVerdict: { kind: "human", rule: "C-23", sites: [{ anchor, element,
|
|
208
|
+
what }] }` (S-5 (i)/(j), C-24, C-28, S-20). The human report lists the sites in an "owed human verdicts"
|
|
209
|
+
section, counted; `--json` prints `{ surface, exitCode, owedHumanVerdicts, results, skipped }` and the
|
|
210
|
+
field is in each result. The `VERDICT DUE (C-23)` prose prefix is gone: a consumer counts owed
|
|
211
|
+
verdicts from the field, never from prose.
|
|
212
|
+
|
|
213
|
+
**S-5 (g) and (l)** match their key bare, double-quoted or single-quoted. **A-11** calls a typed
|
|
214
|
+
literal any quoted `*.claudemcpcontent.com` (any label, any case, any quoting, a scheme, several
|
|
215
|
+
labels, or a literal joined to the suffix); the bare suffix constant and an interpolated label are not
|
|
216
|
+
literals.
|
|
217
|
+
|
|
218
|
+
Amended rules, adapted in place: **C-2** (a tool name is at most 64 characters), **C-15** (a `title`,
|
|
219
|
+
and no enumerated parameter spanning a read and a write verb), **C-16** (event payloads), **S-3**
|
|
220
|
+
(`skills/list` and `skills/get`), **S-4** (the closed trigger: manifest `minProtocolVersion >=
|
|
221
|
+
2026-07-28`, the `events` capability, a host extension), **E-1** (the vendor-namespace registry
|
|
222
|
+
`vendor-namespaces.json`, `openai/` first member, shipped in the package and read by the detector),
|
|
223
|
+
**E-3** (the fourth type, `host-capability-adapter`), and, with new detectors because they never had
|
|
224
|
+
one, **S-12**, **A-2** and **A-7**. **C-6** now flags a log of an environment variable that names a
|
|
225
|
+
URL / endpoint / host, and of `process.cwd()` and the other absolute-path sources, through
|
|
226
|
+
`console.*`, `logger.*` and `process.stderr|stdout.write`, directly or through an identifier, a
|
|
227
|
+
destructured binding or an object property.
|
|
228
|
+
|
|
229
|
+
The coverage grid (`analysis/mcp-doctor-coverage-grid.md`) is derived by
|
|
230
|
+
`npx tsx scripts/standard-coverage.ts <pin> <ElPi-Corp checkout>`; the foreign-material bite proof is
|
|
231
|
+
`npm run test:v070-foreign-bite` and `npm run test:v080-foreign-bite` (see `scripts/v070-foreign-bite-probe.sh` and `scripts/v080-foreign-bite-probe.sh`).
|
|
232
|
+
|
|
128
233
|
**DLocalIdentity exemptions.** Two, both reported as `EXEMPT` rather than hidden: the
|
|
129
234
|
`@vantageos/cloud-identity` package itself (read from the target's `package.json` name), and a site
|
|
130
235
|
carrying `// allow-local-identity: <reason of 8 characters or more>` on the line, in the comment
|
package/dist/doctor.d.ts
CHANGED
|
@@ -50,9 +50,47 @@ interface Tree {
|
|
|
50
50
|
* so when it finds none. Unset means "not supplied", never "no backend".
|
|
51
51
|
*/
|
|
52
52
|
backendRoot?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Product-looking source the scan did NOT read (`.js` `.mjs` `.cjs` `.jsx` `.mts` `.cts`, relative paths;
|
|
55
|
+
* declarations, tests and config files excluded). A rule that says `absent` on the strength of the files
|
|
56
|
+
* it read must not say it while this list is non-empty: it says could-not-judge and names them.
|
|
57
|
+
*/
|
|
58
|
+
unscannedSource?: string[];
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
type State = "present" | "absent" | "non-conforming" | "could-not-judge";
|
|
62
|
+
/**
|
|
63
|
+
* One site on which a human verdict is due (C-23): the mechanical half of the rule ran in full and
|
|
64
|
+
* the remaining question is judgement, which an instrument may not answer.
|
|
65
|
+
*/
|
|
66
|
+
interface OwedVerdictSite {
|
|
67
|
+
/** file:line the verdict is about. */
|
|
68
|
+
anchor: string;
|
|
69
|
+
/** What the human must decide at that site, in one sentence. */
|
|
70
|
+
what: string;
|
|
71
|
+
/** The rule element the site belongs to, when the rule has lettered elements (e.g. "S-5 (i)"). */
|
|
72
|
+
element?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* "A human verdict is owed", as a STRUCTURED field of a could-not-judge result. There is NO fifth
|
|
76
|
+
* state: C-23's "sites exposed, verdict due" is a could-not-judge whose owed half is judgement, and
|
|
77
|
+
* a consumer counts owed verdicts by reading this field, never by scanning prose (derive-never-type).
|
|
78
|
+
* Only a could-not-judge may carry it, and never with zero sites (a zero is an event, not a rest).
|
|
79
|
+
*/
|
|
80
|
+
interface OwedVerdict {
|
|
81
|
+
kind: "human";
|
|
82
|
+
/** The rule that makes the verdict owed: C-23 (a judgement control exposes its sites). */
|
|
83
|
+
rule: string;
|
|
84
|
+
/**
|
|
85
|
+
* True when the detector ran the whole mechanical half of its rule and the ONLY thing left is the
|
|
86
|
+
* human verdict on `sites` (C-23: "green is: the sites are exposed"). The exit code then treats the
|
|
87
|
+
* result as OWED, not unreadable: it does not raise exit 2, and the sites stay printed and counted
|
|
88
|
+
* (`owedHumanVerdicts`). Absent or false: something besides the human verdict is unread (a probe, a
|
|
89
|
+
* dynamic half), and the result exits 2 like any could-not-judge.
|
|
90
|
+
*/
|
|
91
|
+
mechanicalHalfComplete?: boolean;
|
|
92
|
+
sites: OwedVerdictSite[];
|
|
93
|
+
}
|
|
56
94
|
interface DetectorResult {
|
|
57
95
|
id: string;
|
|
58
96
|
/** display anchor(s) file:line — the site the verdict is about. */
|
|
@@ -65,6 +103,8 @@ interface DetectorResult {
|
|
|
65
103
|
* and the runner refuses it. Ignored for the three decisive states.
|
|
66
104
|
*/
|
|
67
105
|
reason?: string;
|
|
106
|
+
/** Present only on a could-not-judge whose owed half is a human verdict. See `OwedVerdict`. */
|
|
107
|
+
owedVerdict?: OwedVerdict;
|
|
68
108
|
}
|
|
69
109
|
/** A single tool as reported by the RUNNING server over the MCP `tools/list` call. */
|
|
70
110
|
interface RuntimeTool {
|
|
@@ -134,7 +174,7 @@ interface Detector {
|
|
|
134
174
|
* so the pre-existing detectors that predate this field still compile; every
|
|
135
175
|
* detector built or re-pinned against a standard rule MUST populate it.
|
|
136
176
|
* Coverage of the standard is DERIVED from this field across the registry
|
|
137
|
-
* (see `scripts/
|
|
177
|
+
* (see `scripts/standard-coverage.ts`) — never from a hand-kept list.
|
|
138
178
|
*/
|
|
139
179
|
rule?: string;
|
|
140
180
|
/** The standard commit this detector's `rule` was read against. Required
|
|
@@ -285,6 +325,20 @@ declare function runDoctorAsync(target: string, opts?: {
|
|
|
285
325
|
*/
|
|
286
326
|
declare function renderSkipped(skipped: SkippedDir[]): string;
|
|
287
327
|
declare function renderTable(surface: string, results: DetectorResult[], skipped?: SkippedDir[]): string;
|
|
328
|
+
/** Total owed human-verdict sites across the results — counted from the structured field. */
|
|
329
|
+
declare function countOwedHumanVerdicts(results: DetectorResult[]): number;
|
|
330
|
+
/**
|
|
331
|
+
* The owed-human-verdicts section: every site a human must decide, counted, read from the
|
|
332
|
+
* structured `owedVerdict` field. Empty string (section omitted) when none are owed — a count of
|
|
333
|
+
* zero is printed by the JSON output, which always carries it.
|
|
334
|
+
*/
|
|
335
|
+
declare function renderOwedVerdicts(results: DetectorResult[]): string;
|
|
336
|
+
/**
|
|
337
|
+
* The machine-readable report (`--json`). The same results the table prints, plus the exit code and
|
|
338
|
+
* the owed-human-verdict count, so a consumer reads "a verdict is owed" from a field and never from
|
|
339
|
+
* prose.
|
|
340
|
+
*/
|
|
341
|
+
declare function renderJson(surface: string, results: DetectorResult[], skipped: SkippedDir[]): string;
|
|
288
342
|
/**
|
|
289
343
|
* The process exit code a run's results carry, three-way with precedence:
|
|
290
344
|
* 1 — a VIOLATION: at least one detector is `non-conforming` (something IS
|
|
@@ -297,4 +351,4 @@ declare function renderTable(surface: string, results: DetectorResult[], skipped
|
|
|
297
351
|
*/
|
|
298
352
|
declare function computeExitCode(results: DetectorResult[]): number;
|
|
299
353
|
|
|
300
|
-
export { type LiveDetector, type RunDoctorOptions, type RunMode, type Session, computeExitCode, isLiveDetector, renderSkipped, renderTable, runDoctor, runDoctorAsync, runDoctorReport };
|
|
354
|
+
export { type LiveDetector, type RunDoctorOptions, type RunMode, type Session, computeExitCode, countOwedHumanVerdicts, isLiveDetector, renderJson, renderOwedVerdicts, renderSkipped, renderTable, runDoctor, runDoctorAsync, runDoctorReport };
|