@vantageos/mcp-doctor 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +214 -0
- package/README.md +96 -5
- package/dist/doctor.d.ts +118 -1
- package/dist/doctor.js +8973 -4970
- package/package.json +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,220 @@
|
|
|
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.1 — 2026-10-13
|
|
7
|
+
|
|
8
|
+
Standard re-pinned to ElPi-Corp `6d463a1ecc5256cd59dd1afe5aa5e335accacc90`: the direct child of
|
|
9
|
+
`24c1ffc97375581c05c1e8058afb2faf78da602e` (mcp-standard v2.6) that adds S-35 and nothing else
|
|
10
|
+
(`gh api repos/elpiarthera/ElPi-Corp/compare/24c1ffc9...6d463a1e` -> ahead 1, behind 0). Coverage is
|
|
11
|
+
derived, never typed here:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
npx tsx scripts/standard-coverage.ts 6d463a1ecc5256cd59dd1afe5aa5e335accacc90 <ElPi-Corp checkout>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **S35** scope-in-read (S-35): a tool's scope predicate is applied inside the read it delegates to.
|
|
19
|
+
Reads every `<client>.query|action(<door>, <args>)` and the narrowing that follows it in the same
|
|
20
|
+
block, in each form the rule names: `scopeFilterList`, `scopeFilterGet`, a `.filter` reading an
|
|
21
|
+
organisation / namespace / recipient / owner field or the caller context, a skipping branch in a loop.
|
|
22
|
+
Red when the call's arguments carry no scope predicate; a door named by an expression, or arguments
|
|
23
|
+
spread from an unread value, is could-not-judge.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- **C-11 (b), dynamic** (v2.6, SEP-1303): the introspector sends an invalid cursor to every protocol list
|
|
27
|
+
method the server declares (`-32602` required) and as the `cursor` argument of every served tool that
|
|
28
|
+
takes one (a tool result with `isError: true` and `structuredContent.error.code` `invalid_cursor`
|
|
29
|
+
required). A protocol error, a silent page, a missing or different code is red; no answer, or a tool
|
|
30
|
+
that requires another argument, is could-not-judge. The outcome domain is a typed union judged by an
|
|
31
|
+
exhaustive switch.
|
|
32
|
+
- **C-9** accepts the v2.6 read verb `prepare`; **C-15** partitions it as a read verb.
|
|
33
|
+
- **C-13** reads the context ceiling in every declaration form: the server manifest's `contextCeiling`
|
|
34
|
+
(`mcp.json` / `mcp-server.json`) and a `CONTEXT_CEILING` / `contextCeiling` const or field, inline or
|
|
35
|
+
behind a same-file const. A form present but unreadable (a manifest that does not parse, a computed
|
|
36
|
+
value) is named: could-not-judge when nothing readable is declared, named in the verdict otherwise.
|
|
37
|
+
Two readable ceilings that disagree are could-not-judge. `findCeiling` (C-19, C-21) reads the same census.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **A target with nothing to read exits 2, not 1** (Theta, task k17dna3w0a5jsq3vezmbwexpmh8gy1nb). A
|
|
41
|
+
missing path used to crash on an uncaught `ENOENT`, which Node exits as 1, the code for "a
|
|
42
|
+
violation". An empty directory was judged, and its absent README and lockfile were reported as
|
|
43
|
+
violations. `loadTree` now throws `SubjectUnreadableError`, and the CLI answers
|
|
44
|
+
`could not judge … (exit 2)` for every form a target can take that yields no readable file:
|
|
45
|
+
- the stat fails: missing, permission, a path through a file;
|
|
46
|
+
- the directory cannot be listed;
|
|
47
|
+
- the directory is empty;
|
|
48
|
+
- the walk reaches no file;
|
|
49
|
+
- the target is a file other than `.ts`/`.tsx`;
|
|
50
|
+
- the target is neither a file nor a directory.
|
|
51
|
+
|
|
52
|
+
A directory holding one file is still judged, and a real violation there still exits 1. Inside
|
|
53
|
+
a readable subject, a subdirectory that cannot be listed, or an entry that cannot be read, is
|
|
54
|
+
now named in `skipped directories` instead of being dropped in silence.
|
|
55
|
+
- **S-22** (Argus, #56): an `allowedOrigins` element bound to a const — an identifier or a spread —
|
|
56
|
+
resolves to the literal it holds, same file first; a wildcard or an empty list behind a const is red.
|
|
57
|
+
A type annotation (`allowedOrigins: readonly string[];`) is no value and no longer reads as an
|
|
58
|
+
unresolved one.
|
|
59
|
+
- **DLocalIdentity** (Eta, vantage-mcp-boilerplate #14): a bare hash over local content — file bytes, a
|
|
60
|
+
directory listing, a digest of them — is a content fingerprint of the tree, not a credential digest.
|
|
61
|
+
Told apart by what the hash consumes, never by a file or function name; a held hash that is updated
|
|
62
|
+
with anything else, or handed elsewhere, stays a site, and so does every keyed primitive.
|
|
63
|
+
- **D3 / D4**: a `content: [` site is attributed to the tool whose extent CONTAINS it (a registration
|
|
64
|
+
call's parentheses, a descriptor object), never to the last tool name seen above it. An unrelated
|
|
65
|
+
`{ name: "list_items" }` row no longer turns a shared content helper into a list builder.
|
|
66
|
+
- **S35** (Eta, #57 REVISE): the narrowing is read when the returned value is not bound by
|
|
67
|
+
`const x = await <call>`. Three forms that fell through to `present` are now judged: the call as an
|
|
68
|
+
argument of `scopeFilterList` / `scopeFilterGet` or the receiver of a `.filter` chain; a `.then(cb)`
|
|
69
|
+
chained on the call (cb's body is read; a callback the scan cannot read is could-not-judge, naming
|
|
70
|
+
the site); one plain rebinding `const y = x`. Narrowing present over an unscoped read is red;
|
|
71
|
+
narrowing absent in that form stays `present`. Declared, not read: a call passed as an argument of
|
|
72
|
+
`.filter(...)` itself, and a second rebinding.
|
|
73
|
+
- **Foreign-material probe** (Eta, #57 REVISE): `BP_MAIN_REF` is pinned to `45f4fbe2` (no longer
|
|
74
|
+
`origin/main`); the missing G6 SHA default is removed and the G6 substrate for `c13` / `d3` is derived
|
|
75
|
+
by overlaying `fixtures/v081-bp-g6-factory/head/src` on the pinned main export (the manifest form of
|
|
76
|
+
the lost G6 head is re-created and declared as constructed); the G6 substrate is built only when
|
|
77
|
+
`c13` or `d3` is requested, so main-only sections run without it; the `s35` section states what it
|
|
78
|
+
checks at the pinned ref.
|
|
79
|
+
|
|
80
|
+
Foreign-material bite proof: `BP_REPO=<vantage-mcp-boilerplate checkout> npm run test:v081-foreign-bite`
|
|
81
|
+
(`scripts/v081-foreign-bite-probe.sh`; the `c11dyn` section builds and launches the boilerplate).
|
|
82
|
+
|
|
83
|
+
### Security
|
|
84
|
+
|
|
85
|
+
- Upgrade dev toolchain (vitest, vite, esbuild...) — no runtime change; published package has no dependencies.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## 0.8.0 — 2026-10-11
|
|
89
|
+
|
|
90
|
+
Measured against the same standard pin as 0.7.0, `b714129a110237c5521d5269ee61ab919408a17f` (91
|
|
91
|
+
rules). 0.7.0 covered 64 of them; 0.8.0 adds a detector for each of the 27 left, so the registry
|
|
92
|
+
covers all of them:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
npx tsx scripts/standard-coverage.ts b714129a110237c5521d5269ee61ab919408a17f ../elpi-corp-std
|
|
96
|
+
-> 91/91 rules covered by a registered detector (pin b714129a110237c5521d5269ee61ab919408a17f)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The 27 are derived from that script's `UNCOVERED` rows at 0.7.0, not listed from memory: **C-23 C-24
|
|
100
|
+
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
|
|
101
|
+
S-27 · E-5 · A-3 A-4 A-5**. Every one has a RED test committed before its implementation and a bite
|
|
102
|
+
mutation on foreign material, one per form of the guarded thing, with its legitimate pole
|
|
103
|
+
(`scripts/v080-foreign-bite-probe.sh`, `npm run test:v080-foreign-bite`).
|
|
104
|
+
|
|
105
|
+
### Added — one detector per uncovered rule
|
|
106
|
+
- **Trunk gates, static half read, gate half named as owed:** **C23** judgement-sites, **C24**
|
|
107
|
+
acceptance-at-dispatch, **C27** subpackage-types-in-isolation, **C28** deploy-failure-signal.
|
|
108
|
+
- **Server:** **S8** token-passthrough, **S9** confused-deputy-proxy, **S10** ssrf-guard, **S11**
|
|
109
|
+
result-contract, **S13** progress-cancel, **S14** version-negotiation, **S15** completion-security,
|
|
110
|
+
**S16** local-footprint, **S17** forward-auth, **S18** session-integrity, **S19** auth-url-open,
|
|
111
|
+
**S20** scope-minimization, **S21** stdio-proxy-sandbox, **S22** origin-validation, **S23**
|
|
112
|
+
protocol-version-header, **S24** tool-rate-limit, **S25** resource-discipline, **S26**
|
|
113
|
+
list-changed-parity, **S27** elicitation-floor.
|
|
114
|
+
- **Extension:** **E5** extension-isolation. **App:** **A3** sandbox-respected, **A4** wcag-axe-gate,
|
|
115
|
+
**A5** performance-budgets.
|
|
116
|
+
- A dynamique or gate row keeps the 0.7.0 semantics: red where the tree contradicts the rule, `absent`
|
|
117
|
+
only when the subject is provably absent (each matcher carries a positive control and a failed
|
|
118
|
+
control throws), otherwise `could-not-judge` naming the half that is owed. Nothing that has an unread
|
|
119
|
+
half returns `present`.
|
|
120
|
+
- Per-detector reading, limits and verdicts: README, "Detectors added in 0.8.0".
|
|
121
|
+
|
|
122
|
+
### Changed — Argus rulings on 0.7.0 (PR #55)
|
|
123
|
+
1. **An owed human verdict is a structured field, not a state and not a prefix.** There is no fifth
|
|
124
|
+
state. A `could-not-judge` result may carry `owedVerdict: { kind: "human", rule: "C-23", sites:
|
|
125
|
+
[{ anchor, element?, what }] }`; the runner refuses it on any other state or with zero sites. S-5
|
|
126
|
+
(i)/(j) use it in place of the `VERDICT DUE (C-23)` prefix (removed from the prose), and so do C-24,
|
|
127
|
+
C-28 and S-20. The human report prints an "owed human verdicts (N sites)" section; the new `--json`
|
|
128
|
+
flag prints `{ surface, exitCode, owedHumanVerdicts, results, skipped }` with the field in each
|
|
129
|
+
result.
|
|
130
|
+
2. **S-5 (g) and (l) match a quoted key.** `"client_id_metadata_document_supported": true` (and the
|
|
131
|
+
single-quoted spelling, and the same for `authorization_response_iss_parameter_supported`) was a
|
|
132
|
+
false red; each spelling x each key x `true` / `false` is now a test.
|
|
133
|
+
3. **A-11 recognises every literal form of the sandbox domain.** Any quoted `*.claudemcpcontent.com`
|
|
134
|
+
with a label in front of the suffix is a typed literal: any label (hex or not, any case, hyphens,
|
|
135
|
+
several labels), any quoting (double, single, a template with no interpolation), a scheme prefix,
|
|
136
|
+
and a literal joined to the suffix (`"vp" + ".claudemcpcontent.com"`). The bare suffix constant and
|
|
137
|
+
an interpolated label are not literals. One test per form.
|
|
138
|
+
|
|
139
|
+
### Fixed — found by running the new detectors on foreign material
|
|
140
|
+
Each was a false positive or a false green found on `vantage-mcp-boilerplate` or `vantage-peers`,
|
|
141
|
+
turned into a RED test before the fix:
|
|
142
|
+
- **C-28**: a deploy named in a YAML comment is not a deploy step.
|
|
143
|
+
- **C-27**: a workflow is read for the commands its `run:` keys run (inline and block), not for every
|
|
144
|
+
line holding the word; an `echo`, a job name or a comment is not a type check or an install.
|
|
145
|
+
- **S-8, S-24**: a marker word inside a string (an error message, a description) is prose, not an
|
|
146
|
+
audience check or a rate guard; a quoted property key still counts.
|
|
147
|
+
- **S-10**: a blocklist written as CIDR pairs (`["10.0.0.0", 8]`, what `node:net` BlockList takes) states
|
|
148
|
+
the union; the two metadata hostnames are satisfied by name resolution; a host classification
|
|
149
|
+
(`startsWith("127.")` choosing http or https) is not address validation outside a file that reaches
|
|
150
|
+
out.
|
|
151
|
+
- **S-13**: a TypeScript method signature named `fetch(request: Request, ...)` is not an outbound call.
|
|
152
|
+
- **S-17**: `iss` is validated in the file that redeems the code, not anywhere in the tree.
|
|
153
|
+
- **S-22**: a quoted `"0.0.0.0"` in a blocklist constant is not a bind; CORS `origin: "*"` is not an
|
|
154
|
+
Origin validation.
|
|
155
|
+
- **S-25**: an interpolated `${proto}://${host}` is a computed URL, not a custom scheme.
|
|
156
|
+
- **A-3, A-4**: a tree that serves `ui://` resources and holds no UI code is `could-not-judge`, not a
|
|
157
|
+
verdict on code that is not there.
|
|
158
|
+
|
|
159
|
+
### Revised after review (PR #56, Argus REVISE at `b60df486`)
|
|
160
|
+
**The exit-0 pole is reachable.** `computeExitCode` returned 2 on any `could-not-judge`, and six new rules
|
|
161
|
+
had no way to say `present`, so no server that registered a tool could be approved. Each rule now declares
|
|
162
|
+
its choice, in its detector header and here:
|
|
163
|
+
- **S-11, S-13, S-16, A-4, A-5, A-3 — choice (b), a static positive path on positive evidence.** `present`
|
|
164
|
+
names the half it read (`static half: ...`) and the half it did NOT read (`NOT read by this run: ...`);
|
|
165
|
+
nothing is claimed about the unread half.
|
|
166
|
+
- S-11: every registered tool declares an `outputSchema` and no error code contradicts; a tool with none
|
|
167
|
+
whose window shows no result shape is `could-not-judge`, named.
|
|
168
|
+
- S-13: every outbound call carries a timeout (or none is emitted), no stale id, no answering cancel handler.
|
|
169
|
+
- S-16: no read outside the object, and a transport posture is identified (stdio, HTTP with a token check, a
|
|
170
|
+
Unix socket); tools with no identifiable transport are `could-not-judge`.
|
|
171
|
+
- A-4: an axe dependency and something that runs it; the CI result stays the gate's.
|
|
172
|
+
- A-5: the measured gzipped size is under 250 KB; with no built bundle in the tree it is `could-not-judge`.
|
|
173
|
+
- A-3: UI code, HTML shells and HTML held in a string literal (`ui://` served from `text: \`<html>\``) are
|
|
174
|
+
read; `ui://` served with no UI code anywhere stays `could-not-judge` (the code is elsewhere).
|
|
175
|
+
- S-14 (not in the review's list, same defect, found by the green-pole test): an SDK-first server whose
|
|
176
|
+
tree shows the official SDK (import or dependency) is `present` on its static half; a server class with
|
|
177
|
+
no SDK evidence stays `could-not-judge`.
|
|
178
|
+
- **S-20 — choice (c), an owed human verdict.** A result whose only open question is the human verdict on the
|
|
179
|
+
exposed sites declares `owedVerdict.mechanicalHalfComplete: true`. **Exit-code treatment, defined and
|
|
180
|
+
tested (`tests/v080-exit-code-owed.test.ts`):** such a result is OWED, not unreadable. It does not raise
|
|
181
|
+
exit 2 (exit 0 when nothing else does) and its sites stay printed and counted in `owedHumanVerdicts`. A
|
|
182
|
+
`could-not-judge` with no such declaration (S-5, whose probes are also owed) exits 2 as before, and a
|
|
183
|
+
violation still exits 1. Not defined: S-23 and S-10 on an SDK HTTP server still end in `could-not-judge`
|
|
184
|
+
(the SDK version that validates the header is not readable from the tree) — they hold exit 2 for such a tree.
|
|
185
|
+
- `reconcile()` (the `--mode=both` divergence) now carries the owed sites of either side.
|
|
186
|
+
- `fixtures/oracle-green-conforming` is repaired to conform to the 0.8.0 rules it failed: a real token-bucket
|
|
187
|
+
rate guard on the tool path (S-24), a stdio transport, a bridge-only UI shell, an axe gate, a built bundle.
|
|
188
|
+
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
|
|
189
|
+
is removed.
|
|
190
|
+
|
|
191
|
+
**Two security detectors no longer fail open.**
|
|
192
|
+
- **S-8** read the incoming Authorization header only through `.get("authorization")` or a quoted bracket, so
|
|
193
|
+
`authorization: c.req.raw.headers.authorization` forwarded to `fetch` read `present`. The census of reads
|
|
194
|
+
(a call, a bracket, DOT access with or without optional chaining, a destructuring renamed or not, any case)
|
|
195
|
+
is one test per form.
|
|
196
|
+
- **S-22** accepted a bare `allowedOrigins:` as protection. It is now read by VALUE: a wildcard (bare, in a
|
|
197
|
+
list, as a host pattern) and an empty list are red; a list of concrete origins is `present`; a variable is
|
|
198
|
+
resolved in the tree (wildcard / empty / concrete) or reported `could-not-judge` naming it; the protection
|
|
199
|
+
flag with no list is red. One test per shape.
|
|
200
|
+
|
|
201
|
+
**Sweep.**
|
|
202
|
+
- Source the scan does not read (`.js .mjs .cjs .jsx .mts .cts`; declarations, tests, config and
|
|
203
|
+
`_generated/` excluded): the code-reading rules (C-23, S-8..S-27, E-5, A-3) say `could-not-judge` naming the
|
|
204
|
+
files instead of `absent`. `src/bin/**` is product source (only a top-level `bin/` is tooling), and tools
|
|
205
|
+
registered by tooling are not the server's.
|
|
206
|
+
- `present` on a word: S-9 (needs a consent lookup, an equality test on `redirect_uri`, a random state), S-20
|
|
207
|
+
(a bare challenge declares no scope), S-24 (a rate-limit identifier must be CALLED), S-17 (the issuer is a
|
|
208
|
+
word of the store key, not the letters of `permissionId`), S-16 (a `footprint` KEY with content, not the
|
|
209
|
+
word), E-5 (zero files read is `could-not-judge`).
|
|
210
|
+
- Comment text as evidence: C-23 (a genre is declared by a code literal or a comment opening with `genre:`),
|
|
211
|
+
A-4 (workflow and test-file comments are not a run).
|
|
212
|
+
- Silent skips: S-15 (case-clause handlers are read; unreadable sites are named), A-5 (oversize, dangling or
|
|
213
|
+
unreadable UI paths are `could-not-judge`, named), C-27 (an unreadable deployable directory is named).
|
|
214
|
+
|
|
215
|
+
### Process
|
|
216
|
+
- `tests/__oracle__/oracle-complete-server.static.json` is regenerated: 27 rows added, no existing row
|
|
217
|
+
changed.
|
|
218
|
+
- Version `0.8.0` is strictly above the live `latest` (`0.7.0`), read from the registry.
|
|
219
|
+
|
|
6
220
|
## 0.7.0 — 2026-10-10
|
|
7
221
|
|
|
8
222
|
Measured against the ElPi-Corp standard pinned at `b714129a110237c5521d5269ee61ab919408a17f`
|
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,10 +56,12 @@ 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
|
|
|
62
|
-
exit: 0 clean, 1 a violation, 2 an abstention with no violation
|
|
63
|
+
exit: 0 clean, 1 a violation, 2 an abstention with no violation (including a target
|
|
64
|
+
that names nothing readable: missing, empty, or not a source tree)
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
## States and exit codes
|
|
@@ -77,6 +79,26 @@ The exit code keeps three outcomes distinct, which is what makes the tool safe i
|
|
|
77
79
|
- **1** — at least one violation.
|
|
78
80
|
- **2** — no violation, but at least one detector abstained. **An abstention is never a pass**: it
|
|
79
81
|
means part of the tree was not judged, and the row names which part and why.
|
|
82
|
+
**Exception, defined in 0.8.0:** a `could-not-judge` that carries an `owedVerdict` declaring
|
|
83
|
+
`mechanicalHalfComplete: true` is an OWED human verdict, not an abstention: the detector ran its whole
|
|
84
|
+
mechanical half and exposed the sites a person must decide (C-23: "green is: the sites are exposed"). It does
|
|
85
|
+
not raise exit 2; the sites stay printed and counted (`owedHumanVerdicts`). Any other `could-not-judge`, with
|
|
86
|
+
or without an `owedVerdict`, still exits 2.
|
|
87
|
+
|
|
88
|
+
**A target with nothing to read exits 2, and no detector runs.** The doctor stops before judging
|
|
89
|
+
anything, prints `could not judge <target> — <what it found>` on stderr, and exits 2. Under `--json`,
|
|
90
|
+
stdout carries `exitCode: 2`, the same text in `subjectUnreadable`, and an empty `results`. This
|
|
91
|
+
covers six cases:
|
|
92
|
+
- the path is missing, or cannot be read;
|
|
93
|
+
- the path is a directory that cannot be listed;
|
|
94
|
+
- the path is an empty directory;
|
|
95
|
+
- the path is a directory where no file can be reached;
|
|
96
|
+
- the path is a file other than `.ts`/`.tsx`;
|
|
97
|
+
- the path is something that is neither a file nor a directory.
|
|
98
|
+
|
|
99
|
+
A directory holding even one file is a subject and is judged, so a missing README or lockfile there is
|
|
100
|
+
still a violation (exit 1). Inside a judged tree, a subdirectory that cannot be listed is named under
|
|
101
|
+
`skipped directories`; it is never dropped in silence.
|
|
80
102
|
|
|
81
103
|
**One known limit in 0.3.2, stated rather than hidden:**
|
|
82
104
|
|
|
@@ -122,7 +144,7 @@ Each detector maps one standard item and is proven **bipolar** on real material:
|
|
|
122
144
|
| **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
145
|
| **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
146
|
| **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 |
|
|
147
|
+
| **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, or over local content — file bytes, a directory listing, a digest of them, i.e. a content fingerprint of the built tree — 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
148
|
| **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
149
|
|
|
128
150
|
### Detectors added in 0.7.0 (standard `b714129a`, ElPi-Corp #243)
|
|
@@ -140,7 +162,7 @@ where the tree contradicts the rule, and otherwise `could-not-judge` naming the
|
|
|
140
162
|
| **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
163
|
| **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
164
|
| **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 (
|
|
165
|
+
| **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) |
|
|
144
166
|
| **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
167
|
| **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
168
|
| **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,6 +175,75 @@ where the tree contradicts the rule, and otherwise `could-not-judge` naming the
|
|
|
153
175
|
| **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
176
|
| **A12** file-write | A-12 | `openai/resources/write`: `ifMatch`, `conflict` + `too-large` handled, received URI only | absent / present / non-conforming |
|
|
155
177
|
|
|
178
|
+
### Detectors added in 0.8.0 (standard `b714129a`) — the 27 rules 0.7.0 left uncovered
|
|
179
|
+
|
|
180
|
+
One detector per remaining rule, so `scripts/standard-coverage.ts` derives **91/91**. Each keeps the
|
|
181
|
+
semantics of a dynamique row: red where the tree contradicts the rule, `absent` only when the subject
|
|
182
|
+
is provably absent (a positive control proves the matcher can see it), otherwise `could-not-judge`
|
|
183
|
+
naming the half that is owed. Comments never count; where a marker word could be prose (a description,
|
|
184
|
+
an error message) it counts only as code or as a quoted property key.
|
|
185
|
+
|
|
186
|
+
| ID | Rule | The static run reads | Verdicts |
|
|
187
|
+
|----|------|----------------------|----------|
|
|
188
|
+
| **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 |
|
|
189
|
+
| **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`) |
|
|
190
|
+
| **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) |
|
|
191
|
+
| **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`) |
|
|
192
|
+
| **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 |
|
|
193
|
+
| **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 |
|
|
194
|
+
| **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) |
|
|
195
|
+
| **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) |
|
|
196
|
+
| **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) |
|
|
197
|
+
| **S14** version-negotiation | S-14 | a hand-rolled `initialize` answering a fixed `protocolVersion` | absent / non-conforming / could-not-judge (the negotiation probe is owed) |
|
|
198
|
+
| **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) |
|
|
199
|
+
| **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) |
|
|
200
|
+
| **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 |
|
|
201
|
+
| **S18** session-integrity | S-18 | `sessionIdGenerator`: predictable, not a CSPRNG, not bound `<user>:<id>`; a session lookup used as authentication | absent / present / non-conforming |
|
|
202
|
+
| **S19** auth-url-open | S-19 | URL opening through a shell, scheme validation by blocklist, no http(s) allowlist | absent / present / non-conforming |
|
|
203
|
+
| **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) |
|
|
204
|
+
| **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 |
|
|
205
|
+
| **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 |
|
|
206
|
+
| **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) |
|
|
207
|
+
| **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 |
|
|
208
|
+
| **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 |
|
|
209
|
+
| **S26** list-changed-parity | S-26 | `resources` / `prompts` declaring `listChanged: true` against the matching `list_changed` emission, per kind | absent / present / non-conforming |
|
|
210
|
+
| **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) |
|
|
211
|
+
| **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 |
|
|
212
|
+
| **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) |
|
|
213
|
+
| **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) |
|
|
214
|
+
| **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) |
|
|
215
|
+
|
|
216
|
+
### Changes in 0.8.1 (standard `6d463a1e` = v2.6 `24c1ffc9` + S-35)
|
|
217
|
+
|
|
218
|
+
| ID | Rule | The static run reads | Verdicts |
|
|
219
|
+
|----|------|----------------------|----------|
|
|
220
|
+
| **S35** scope-in-read | S-35 | each `<client>.query\|action(<door>, <args>)` and, in the same block, a narrowing of what it returned (`scopeFilterList`, `scopeFilterGet`, a `.filter` on an organisation / namespace / recipient / owner field or the caller context, a skipping branch in a loop) | absent / present / non-conforming / could-not-judge (door named by an expression) |
|
|
221
|
+
|
|
222
|
+
**C-11 (b)** dynamic: an invalid cursor is sent to each protocol list method (`-32602`) and as the cursor
|
|
223
|
+
argument of each tool that takes one (`isError: true`, `structuredContent.error.code` = `invalid_cursor`).
|
|
224
|
+
**C-9** accepts `prepare` (a read verb). **C-13** reads the ceiling from the server manifest's
|
|
225
|
+
`contextCeiling` as well as from a source constant. **S-22** resolves an `allowedOrigins` element held by a
|
|
226
|
+
same-file const. **DLocalIdentity** does not count a hash over local file content. **D3 / D4** attribute a
|
|
227
|
+
result builder to the tool that contains it.
|
|
228
|
+
|
|
229
|
+
**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
|
|
230
|
+
and A-5 return `present` on a static positive path: the verdict says `static half: ...` and names the half it
|
|
231
|
+
did NOT read (`NOT read by this run: ...`) — the served output conforms, progress is monotone, the negotiation
|
|
232
|
+
probe, the runtime file trace, the CI result of the axe run, render/roundtrip/memory. `--mode=dynamic` does not
|
|
233
|
+
implement those reads. Read `present` on these rows as "the static half holds", never as "the rule is held".
|
|
234
|
+
|
|
235
|
+
**An owed human verdict is a field, not a state.** There is no fifth state. A `could-not-judge` whose
|
|
236
|
+
owed half is judgement carries `owedVerdict: { kind: "human", rule: "C-23", sites: [{ anchor, element,
|
|
237
|
+
what }] }` (S-5 (i)/(j), C-24, C-28, S-20). The human report lists the sites in an "owed human verdicts"
|
|
238
|
+
section, counted; `--json` prints `{ surface, exitCode, owedHumanVerdicts, results, skipped }` and the
|
|
239
|
+
field is in each result. The `VERDICT DUE (C-23)` prose prefix is gone: a consumer counts owed
|
|
240
|
+
verdicts from the field, never from prose.
|
|
241
|
+
|
|
242
|
+
**S-5 (g) and (l)** match their key bare, double-quoted or single-quoted. **A-11** calls a typed
|
|
243
|
+
literal any quoted `*.claudemcpcontent.com` (any label, any case, any quoting, a scheme, several
|
|
244
|
+
labels, or a literal joined to the suffix); the bare suffix constant and an interpolated label are not
|
|
245
|
+
literals.
|
|
246
|
+
|
|
156
247
|
Amended rules, adapted in place: **C-2** (a tool name is at most 64 characters), **C-15** (a `title`,
|
|
157
248
|
and no enumerated parameter spanning a read and a write verb), **C-16** (event payloads), **S-3**
|
|
158
249
|
(`skills/list` and `skills/get`), **S-4** (the closed trigger: manifest `minProtocolVersion >=
|
|
@@ -166,7 +257,7 @@ destructured binding or an object property.
|
|
|
166
257
|
|
|
167
258
|
The coverage grid (`analysis/mcp-doctor-coverage-grid.md`) is derived by
|
|
168
259
|
`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`).
|
|
260
|
+
`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`).
|
|
170
261
|
|
|
171
262
|
**DLocalIdentity exemptions.** Two, both reported as `EXEMPT` rather than hidden: the
|
|
172
263
|
`@vantageos/cloud-identity` package itself (read from the target's `package.json` name), and a site
|
package/dist/doctor.d.ts
CHANGED
|
@@ -50,9 +50,70 @@ 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[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The target names nothing the doctor can read, so there is no subject to judge. Thrown by
|
|
62
|
+
* `loadTree` before any detector runs; the CLI turns it into exit 2 ("could not judge"), never 1:
|
|
63
|
+
* a verdict of "violation" about a tree that was never read is a finding manufactured from nothing.
|
|
64
|
+
*
|
|
65
|
+
* The forms a target can take come from `fs.statSync` and `fs.readdirSync`, and every one of them
|
|
66
|
+
* that yields no readable file ends here, named:
|
|
67
|
+
* - stat fails (does not exist, permission, a path through a file, a loop);
|
|
68
|
+
* - a directory that cannot be listed;
|
|
69
|
+
* - a directory with no entries;
|
|
70
|
+
* - a directory whose walk reaches no readable file (only empty, skipped, unreadable or
|
|
71
|
+
* non-regular entries — a FIFO, socket or device is never read: reading one can block);
|
|
72
|
+
* - a regular file that is not `.ts` / `.tsx` (the only single files the scan reads);
|
|
73
|
+
* - a `.ts` / `.tsx` file that cannot be read;
|
|
74
|
+
* - anything that is neither a directory nor a regular file.
|
|
75
|
+
* A directory holding at least one file IS a subject: the detectors judge it, and a missing README
|
|
76
|
+
* or lockfile there is a real violation.
|
|
77
|
+
*/
|
|
78
|
+
declare class SubjectUnreadableError extends Error {
|
|
79
|
+
readonly target: string;
|
|
80
|
+
readonly form: string;
|
|
81
|
+
constructor(target: string, form: string);
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
type State = "present" | "absent" | "non-conforming" | "could-not-judge";
|
|
85
|
+
/**
|
|
86
|
+
* One site on which a human verdict is due (C-23): the mechanical half of the rule ran in full and
|
|
87
|
+
* the remaining question is judgement, which an instrument may not answer.
|
|
88
|
+
*/
|
|
89
|
+
interface OwedVerdictSite {
|
|
90
|
+
/** file:line the verdict is about. */
|
|
91
|
+
anchor: string;
|
|
92
|
+
/** What the human must decide at that site, in one sentence. */
|
|
93
|
+
what: string;
|
|
94
|
+
/** The rule element the site belongs to, when the rule has lettered elements (e.g. "S-5 (i)"). */
|
|
95
|
+
element?: string;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* "A human verdict is owed", as a STRUCTURED field of a could-not-judge result. There is NO fifth
|
|
99
|
+
* state: C-23's "sites exposed, verdict due" is a could-not-judge whose owed half is judgement, and
|
|
100
|
+
* a consumer counts owed verdicts by reading this field, never by scanning prose (derive-never-type).
|
|
101
|
+
* Only a could-not-judge may carry it, and never with zero sites (a zero is an event, not a rest).
|
|
102
|
+
*/
|
|
103
|
+
interface OwedVerdict {
|
|
104
|
+
kind: "human";
|
|
105
|
+
/** The rule that makes the verdict owed: C-23 (a judgement control exposes its sites). */
|
|
106
|
+
rule: string;
|
|
107
|
+
/**
|
|
108
|
+
* True when the detector ran the whole mechanical half of its rule and the ONLY thing left is the
|
|
109
|
+
* human verdict on `sites` (C-23: "green is: the sites are exposed"). The exit code then treats the
|
|
110
|
+
* result as OWED, not unreadable: it does not raise exit 2, and the sites stay printed and counted
|
|
111
|
+
* (`owedHumanVerdicts`). Absent or false: something besides the human verdict is unread (a probe, a
|
|
112
|
+
* dynamic half), and the result exits 2 like any could-not-judge.
|
|
113
|
+
*/
|
|
114
|
+
mechanicalHalfComplete?: boolean;
|
|
115
|
+
sites: OwedVerdictSite[];
|
|
116
|
+
}
|
|
56
117
|
interface DetectorResult {
|
|
57
118
|
id: string;
|
|
58
119
|
/** display anchor(s) file:line — the site the verdict is about. */
|
|
@@ -65,6 +126,8 @@ interface DetectorResult {
|
|
|
65
126
|
* and the runner refuses it. Ignored for the three decisive states.
|
|
66
127
|
*/
|
|
67
128
|
reason?: string;
|
|
129
|
+
/** Present only on a could-not-judge whose owed half is a human verdict. See `OwedVerdict`. */
|
|
130
|
+
owedVerdict?: OwedVerdict;
|
|
68
131
|
}
|
|
69
132
|
/** A single tool as reported by the RUNNING server over the MCP `tools/list` call. */
|
|
70
133
|
interface RuntimeTool {
|
|
@@ -84,6 +147,39 @@ interface RuntimeTool {
|
|
|
84
147
|
* eval file has `evalCaseCount` unset, which D8 reads as zero coverage.
|
|
85
148
|
*/
|
|
86
149
|
evalCaseCount?: number;
|
|
150
|
+
/**
|
|
151
|
+
* True when the served `inputSchema` declares a `cursor` property: the tool takes a cursor as an
|
|
152
|
+
* ARGUMENT, which C-11 (b) probes with an invalid value (SEP-1303).
|
|
153
|
+
*/
|
|
154
|
+
cursorArgument?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* What one invalid-cursor probe read (C-11). The domain is the JSON-RPC response shapes: an `error`
|
|
158
|
+
* member, a `result` member (with or without `isError`, with or without
|
|
159
|
+
* `structuredContent.error.code`), no answer before the deadline, or a probe the introspector
|
|
160
|
+
* refused to send (it never invents a value for another required argument).
|
|
161
|
+
*/
|
|
162
|
+
type CursorProbeOutcome = {
|
|
163
|
+
kind: "error";
|
|
164
|
+
code: unknown;
|
|
165
|
+
message?: string;
|
|
166
|
+
} | {
|
|
167
|
+
kind: "result";
|
|
168
|
+
isError: boolean;
|
|
169
|
+
errorCode?: unknown;
|
|
170
|
+
} | {
|
|
171
|
+
kind: "no-answer";
|
|
172
|
+
reason: string;
|
|
173
|
+
} | {
|
|
174
|
+
kind: "not-sent";
|
|
175
|
+
reason: string;
|
|
176
|
+
};
|
|
177
|
+
/** One invalid-cursor probe: where the cursor arrived (C-11 (a) list method, (b) tool argument). */
|
|
178
|
+
interface CursorProbe {
|
|
179
|
+
/** The protocol list method (`tools/list` ...) or the tool name. */
|
|
180
|
+
target: string;
|
|
181
|
+
surface: "list-method" | "tool-argument";
|
|
182
|
+
outcome: CursorProbeOutcome;
|
|
87
183
|
}
|
|
88
184
|
/**
|
|
89
185
|
* What the RUNNING target server exposes, gathered by launching it and speaking
|
|
@@ -126,6 +222,12 @@ interface RuntimeContext {
|
|
|
126
222
|
* length and asserts that shape rather than assuming it.
|
|
127
223
|
*/
|
|
128
224
|
expectedCommit?: string;
|
|
225
|
+
/**
|
|
226
|
+
* The invalid-cursor round trips (C-11): one per protocol list method the server declares, and one
|
|
227
|
+
* per served tool that takes a cursor argument. Unset when the launch failed, or when the
|
|
228
|
+
* introspector recorded none (C-11 reads that as could-not-judge, never a pass).
|
|
229
|
+
*/
|
|
230
|
+
cursorProbes?: CursorProbe[];
|
|
129
231
|
}
|
|
130
232
|
interface Detector {
|
|
131
233
|
id: string;
|
|
@@ -285,6 +387,20 @@ declare function runDoctorAsync(target: string, opts?: {
|
|
|
285
387
|
*/
|
|
286
388
|
declare function renderSkipped(skipped: SkippedDir[]): string;
|
|
287
389
|
declare function renderTable(surface: string, results: DetectorResult[], skipped?: SkippedDir[]): string;
|
|
390
|
+
/** Total owed human-verdict sites across the results — counted from the structured field. */
|
|
391
|
+
declare function countOwedHumanVerdicts(results: DetectorResult[]): number;
|
|
392
|
+
/**
|
|
393
|
+
* The owed-human-verdicts section: every site a human must decide, counted, read from the
|
|
394
|
+
* structured `owedVerdict` field. Empty string (section omitted) when none are owed — a count of
|
|
395
|
+
* zero is printed by the JSON output, which always carries it.
|
|
396
|
+
*/
|
|
397
|
+
declare function renderOwedVerdicts(results: DetectorResult[]): string;
|
|
398
|
+
/**
|
|
399
|
+
* The machine-readable report (`--json`). The same results the table prints, plus the exit code and
|
|
400
|
+
* the owed-human-verdict count, so a consumer reads "a verdict is owed" from a field and never from
|
|
401
|
+
* prose.
|
|
402
|
+
*/
|
|
403
|
+
declare function renderJson(surface: string, results: DetectorResult[], skipped: SkippedDir[]): string;
|
|
288
404
|
/**
|
|
289
405
|
* The process exit code a run's results carry, three-way with precedence:
|
|
290
406
|
* 1 — a VIOLATION: at least one detector is `non-conforming` (something IS
|
|
@@ -297,4 +413,5 @@ declare function renderTable(surface: string, results: DetectorResult[], skipped
|
|
|
297
413
|
*/
|
|
298
414
|
declare function computeExitCode(results: DetectorResult[]): number;
|
|
299
415
|
|
|
300
|
-
export {
|
|
416
|
+
export { SubjectUnreadableError, computeExitCode, countOwedHumanVerdicts, isLiveDetector, renderJson, renderOwedVerdicts, renderSkipped, renderTable, runDoctor, runDoctorAsync, runDoctorReport };
|
|
417
|
+
export type { LiveDetector, RunDoctorOptions, RunMode, Session };
|