@vantageos/mcp-doctor 0.5.0 → 0.6.0

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