@booyaka/mcp-vet 0.10.2 → 0.10.4

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/BENCHMARK.md CHANGED
@@ -1,222 +1,295 @@
1
- # Benchmark: corpus, methodology, and honest limits
2
-
3
- The README claims high precision on real MCP code. This file is the evidence
4
- behind that claim — corpus, pinned commits, counts, labels, and what the
5
- scanner is *known to miss* — so the claim is checkable rather than vibes.
6
-
7
- > Prompted by community feedback on the launch post: *"'0 false positives' is
8
- > encouraging but incomplete without corpus size, commit SHAs, labeled
9
- > negatives, and recall."* Correct. Here they are.
10
-
11
- ## Corpus (pinned)
12
-
13
- Originally scanned with `mcp-vet` v0.4.0 (re-scanned with v0.9.0 above) (`node dist/cli.js <roots> --json`), all rules
14
- enabled, default confidence (`low`), on 2026-07-23:
15
-
16
- | Repo | Commit | Scanned root | Files | LOC |
17
- | --- | --- | --- | --- | --- |
18
- | [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) | `d31124c982401739917fd817c2a59db344529c16` | `src/` | 78 | 14,742 |
19
- | [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) | `1e1392e3f91583884fe82a0b4b91335875c3fba6` | `examples/` | 144 | 17,224 |
20
- | [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) | `3a6f2996cdd8358957479791e8b26198c07d6a75` | `examples/` | 225 | 12,013 |
21
- | **Total** | | | **447** | **43,979** |
22
-
23
- File counts are candidate files (`.ts/.tsx/.js/.mjs/.cjs/.py`) under the
24
- scanned roots, excluding `node_modules`.
25
-
26
- ## Results — v0.10.2 (21-rule engine, auth-hardening rules added)
27
-
28
- Re-run 2026-08-01 with `mcp-vet` v0.10.2 on the SAME pinned corpus:
29
- **243 findings across 66 files** — identical to the v0.9.0 run. By confidence:
30
- 100 high, 142 medium, 1 low.
31
-
32
- | New pattern | Findings |
33
- | --- | --- |
34
- | `AUTH_ISS_UNVALIDATED` | 0 |
35
- | `AUTH_DCR_NO_APPLICATION_TYPE` | 0 |
36
- | `AUTH_CREDENTIALS_NOT_ISSUER_KEYED` | 0 |
37
-
38
- **The three auth-hardening rules produce zero findings on this corpus, and that
39
- is the correct result** — not a sign they do nothing. The official SDK examples
40
- are already compliant: both SDKs supply `application_type`, the example clients
41
- read `iss` from the callback, and the one apparent credential-store hit was an
42
- authorization-server token cache. The rules are deliberately conservative, and
43
- their positive behaviour is proven by fixtures instead — `test/fixtures/auth/`
44
- (a hand-rolled registration + un-validated redemption → exactly 2 findings) and
45
- `test/fixtures/dirty/` (all three, in both analyzers). A corpus of correct code
46
- *should* be quiet; the fixtures are what stop that quiet from being vacuous.
47
-
48
- > **Correction — v0.10.0 overstated this table.** 0.10.0 reported 247 findings
49
- > and labeled three `AUTH_DCR_NO_APPLICATION_TYPE` hits in the python-sdk
50
- > examples as *true* positives. They were false positives, and the rule was
51
- > wrong, not just the label: both official SDKs supply `application_type` for
52
- > you. python-sdk `src/mcp/shared/auth.py` defaults it on
53
- > `OAuthClientMetadata` (*"SEP-837: OIDC application_type. Defaults to
54
- > `"native"`…"*), and typescript-sdk
55
- > `packages/client/src/client/auth.ts:902` derives it
56
- > (`clientMetadata.application_type ?? deriveApplicationType(clientMetadata.redirect_uris)`).
57
- > A body routed through either is already correct. 0.10.1 recognizes those SDK
58
- > symbols and only flags a **hand-rolled** registration POST; the three corpus
59
- > findings are gone, and the shapes are locked as true negatives
60
- > (`negatives/sdk_dcr_defaults.py`, `negatives/sdk-dcr-defaults.ts`). The
61
- > headline below is the corrected measurement.
62
-
63
- Notes on the two rules that could have fired and correctly did not:
64
-
65
- - `AUTH_CREDENTIALS_NOT_ISSUER_KEYED` flagged
66
- `servers/simple-auth/mcp_simple_auth/simple_auth_provider.py:217` in 0.10.0
67
- and 0.10.1 — `self.tokens[mcp_token] = AccessToken(…, client_id=…)`, an
68
- *authorization-server access-token cache* keyed by the token it just minted,
69
- not a client persisting registration credentials. It was counted as a false
70
- positive in both releases; **0.10.2 fixes the rule** (a token-shaped
71
- container or value is exempt unless the stored value carries a
72
- `client_secret` — only registration hands one out). Locked by
73
- `negatives/server_token_cache.py`.
74
- - `AUTH_ISS_UNVALIDATED` produced **zero** corpus findings — correctly. The
75
- SDK example clients either read `iss` from the callback (`main.py`,
76
- `oauth_client.py` — the rule sees the token and stays quiet) or drive the
77
- exchange through SDK helpers with no `grant_type`/`authorization_code`
78
- literal in the file, which is the documented
79
- `adversarial/missed/auth-helper-indirection.ts` recall boundary.
80
-
81
- So the v0.10.2 headline on this corpus is **241/243 true positives (2 FP,
82
- 0.8%)** — exactly the two v0.9.0 FPs, both unrelated to the new rules. The
83
- auth-hardening rules added no false positives and no true positives here; the
84
- precision improvement over 0.10.0's *reported* 1.2% is the removal of three
85
- mislabeled findings and one genuine FP, not a change to the older rules.
86
-
87
- ## Results — v0.9.0 (18-rule engine, final 2026-07-28 changelog)
88
-
89
- Re-run 2026-07-28 with `mcp-vet` v0.9.0 on the SAME pinned corpus:
90
- **243 findings across 66 files** (was 105/41 with the 9-rule engine — the
91
- final changelog's removals fire heavily on the SDKs' own pre-final examples).
92
- By confidence: 100 high, 142 medium, 1 low.
93
-
94
- | Pattern | Findings |
95
- | --- | --- |
96
- | `SSE_RESUMABILITY_REMOVED` | 65 |
97
- | `MCP_SESSION_ID` | 49 |
98
- | `ELICITATION_COMPLETE_REMOVED` | 44 |
99
- | `RESOURCE_SUBSCRIBE_REMOVED` | 23 |
100
- | `SAMPLING_CAP` | 16 |
101
- | `LOGGING_CAP` | 16 |
102
- | `ROOTS_CAP` | 10 |
103
- | `ROOTS_LIST_CHANGED_REMOVED` | 5 |
104
- | `INITIALIZE_HANDLER` | 4 |
105
- | `PING_REMOVED` | 3 |
106
- | `TASKS_LEGACY` | 2 |
107
- | `TASKS_RESULT_REMOVED` | 2 |
108
- | `OAUTH_DCR` | 2 |
109
- | `ERROR_CODE_RENUMBERED` | 1 |
110
- | `LOGGING_SETLEVEL_REMOVED` | 1 |
111
-
112
- Labeling of the 138 NEW findings (spot-reviewed per category against source):
113
-
114
- - **136 true positives.** The corpus repos genuinely implement the removed
115
- surfaces: the typescript-sdk `everything` example ships an
116
- `InMemoryEventStore` + `Last-Event-ID` resumability transport (all 65 SSE
117
- findings sit in those transport/resumability files), the elicitation examples
118
- register `notifications/elicitation/complete` handlers and read
119
- `elicitationId`, `everything/resources/subscriptions.ts` and friends register
120
- `SubscribeRequestSchema`/`UnsubscribeRequestSchema`, and the bearer-auth
121
- clients send `method: 'ping'`.
122
- - **2 counted as false positives (honest reading):**
123
- `guides/serving/sessions-state-scaling.examples.ts:62` returns
124
- `code: -32001` for "Session not found" — an *implementation-defined* use the
125
- final policy grandfathers; static analysis cannot distinguish it from the
126
- renumbered `HeaderMismatch`, so ERROR_CODE_RENUMBERED flags it and we count
127
- it against ourselves. Plus the pre-existing `mcp-session-id` negative
128
- assertion (below). During this run a `registerTool('ping', ...)` false
129
- positive (a tool merely NAMED ping) was found and FIXED before release —
130
- strict registration context now excludes tool/prompt/resource registration
131
- calls, locked into `negatives/`.
132
-
133
- So the v0.9.0 headline on this corpus is **241/243 true positives (2 FP,
134
- 0.8%)** — same discipline as before: FPs are counted, not defined away.
135
-
136
- ## Results — v0.4.0 (9-rule engine, release-candidate era)
137
-
138
- **105 findings across 41 files** (TypeScript/JavaScript: 93, Python: 12).
139
- By confidence: 66 high, 38 medium, 1 low.
140
-
141
- | Pattern | Findings |
142
- | --- | --- |
143
- | `MCP_SESSION_ID` | 49 |
144
- | `LOGGING_CAP` | 17 |
145
- | `SAMPLING_CAP` | 16 |
146
- | `ROOTS_CAP` | 15 |
147
- | `INITIALIZE_HANDLER` | 4 |
148
- | `TASKS_LEGACY` | 2 |
149
- | `TASKS_RESULT_REMOVED` | 2 |
150
-
151
- ### Labeling
152
-
153
- Every finding was manually reviewed against its source line:
154
-
155
- - **104 / 105 true positives** — real references to a removed or deprecated
156
- protocol surface (session headers/ids, handshake registration, legacy task
157
- methods, deprecated capability declarations and method strings).
158
- - **1 / 105 false positive (0.95%)** —
159
- `stories/json_response/client.py:62` in the typescript-sdk examples:
160
- `assert "mcp-session-id" not in response.headers`. That line is
161
- *already-migrated* test code asserting the header is **absent**; flagging it
162
- as "will break" is wrong. It is exactly what inline suppression
163
- (`# mcp-vet-disable-line MCP_SESSION_ID`) is for, but we count it as a false
164
- positive rather than defining it away. So the honest headline is
165
- **"1 false positive in 44k LOC"**, not zero.
166
-
167
- Notes on reading the numbers:
168
-
169
- - Two occurrences on one line (e.g. `transport.sessionId && sessions.delete(transport.sessionId)`)
170
- are reported as two findings — column-level dedup, not line-level.
171
- - Findings in test files (`__tests__/…`) are counted as true positives: a test
172
- that registers `sampling/createMessage` breaks the same way production code
173
- does.
174
-
175
- ### Labeled negatives
176
-
177
- Files asserted to stay **clean** are part of the repo's test suite and run in CI:
178
-
179
- - `test/fixtures/clean/` — a full server written in the 2026-07-28 style
180
- (per-request `_meta`, `sessionIdGenerator: undefined`, `-32602`).
181
- - `test/fixtures/negatives/` — "false friend" patterns: `sessionId` on plain
182
- app-level objects, `-32002` inside strings/comments, capability-like words
183
- with no capabilities context.
184
- - `test/fixtures/adversarial/caught/` — obfuscations the scanner **must**
185
- catch: aliased imports (TS + Python), namespace-qualified SDK constants,
186
- client transports resuming a `sessionId`.
187
-
188
- Additionally, in the corpus above, comment-only mentions (e.g. `Mcp-Session-Id`
189
- in a comment, `initialize` in prose) produced zero findings — the AST layer
190
- distinguishes executable tokens from comments by construction.
191
-
192
- ## Recall — what the scanner is known to miss
193
-
194
- Static token analysis proves known patterns are **absent**; it cannot prove
195
- your server **speaks the new wire contract**. Recall is bounded by
196
- construction, and the misses are locked into the test suite
197
- (`test/fixtures/adversarial/missed/`, asserted to produce zero findings so any
198
- silent claim-inflation fails CI):
199
-
200
- - split/computed method strings — `'tasks' + '/list'`, `` `tasks/${op}` ``, f-strings
201
- - computed capability keys — `{ ['roo'+'ts']: {} }`
202
- - generated/loop-driven registration from string fragments
203
- - framework-adapter indirection (route tables built at runtime)
204
- - cross-module renames — a wrapper re-exporting an SDK constant under a new
205
- name is flagged in the wrapper file, but a consumer importing only the new
206
- name scans clean on its own
207
-
208
- There is no corpus-wide recall *percentage*: that would require a labeled set
209
- of every legacy usage in the wild, which nobody has. What we can say is: for
210
- the pattern shapes listed in the README, detection is exact; for the shapes
211
- above, it is zero, and the tool says so — pair the scan with runtime checks
212
- (`mcp-vet fixtures`) to cover the difference.
213
-
214
- ## Reproducing
215
-
216
- ```bash
217
- git clone --depth 1 https://github.com/modelcontextprotocol/servers
218
- git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk
219
- git clone --depth 1 https://github.com/modelcontextprotocol/python-sdk
220
- # check out the pinned SHAs above, then:
221
- npx @booyaka/mcp-vet servers/src typescript-sdk/examples python-sdk/examples --json --no-files
222
- ```
1
+ # Benchmark: corpus, methodology, and honest limits
2
+
3
+ The README claims high precision on real MCP code. This file is the evidence
4
+ behind that claim — corpus, pinned commits, counts, labels, and what the
5
+ scanner is *known to miss* — so the claim is checkable rather than vibes.
6
+
7
+ > Prompted by community feedback on the launch post: *"'0 false positives' is
8
+ > encouraging but incomplete without corpus size, commit SHAs, labeled
9
+ > negatives, and recall."* Correct. Here they are.
10
+
11
+ ## Corpus (pinned)
12
+
13
+ Originally scanned with `mcp-vet` v0.4.0 (re-scanned with v0.9.0 above) (`node dist/cli.js <roots> --json`), all rules
14
+ enabled, default confidence (`low`), on 2026-07-23:
15
+
16
+ | Repo | Commit | Scanned root | Files | LOC |
17
+ | --- | --- | --- | --- | --- |
18
+ | [modelcontextprotocol/servers](https://github.com/modelcontextprotocol/servers) | `d31124c982401739917fd817c2a59db344529c16` | `src/` | 78 | 14,742 |
19
+ | [modelcontextprotocol/typescript-sdk](https://github.com/modelcontextprotocol/typescript-sdk) | `1e1392e3f91583884fe82a0b4b91335875c3fba6` | `examples/` | 144 | 17,224 |
20
+ | [modelcontextprotocol/python-sdk](https://github.com/modelcontextprotocol/python-sdk) | `3a6f2996cdd8358957479791e8b26198c07d6a75` | `examples/` | 225 | 12,013 |
21
+ | **Total** | | | **447** | **43,979** |
22
+
23
+ File counts are candidate files (`.ts/.tsx/.js/.mjs/.cjs/.py`) under the
24
+ scanned roots, excluding `node_modules`.
25
+
26
+ ## Validation against real-world hand-rolled OAuth clients (v0.10.3)
27
+
28
+ The pinned corpus above is almost entirely **SDK-routed** code, so it cannot
29
+ exercise the auth-hardening rules: the SDKs satisfy the requirements for you.
30
+ The rules exist for clients that do OAuth **by hand**, so that population was
31
+ sampled separately — three MCP OAuth clients found via GitHub code search
32
+ (`mcp-use/mcp-use`, `greeves89/AI-Employee`, `ogx-ai/ogx`), each read against
33
+ source.
34
+
35
+ | | v0.10.2 | v0.10.3 |
36
+ | --- | --- | --- |
37
+ | Findings | 4 | 2 |
38
+ | True positives | 2 (both on the **wrong line**) | **2, correctly anchored** |
39
+ | False positives | 2 | **0** |
40
+
41
+ The exercise found three defects the 447-file corpus never surfaced:
42
+
43
+ 1. **Wrong anchor line.** `AUTH_ISS_UNVALIDATED` reported the DCR body's
44
+ `grant_types: ["authorization_code"]` at `ogx…:85`; the code is actually
45
+ redeemed at `:143`. `AUTH_DCR_NO_APPLICATION_TYPE` reported an unposted
46
+ local dict at `:58` instead of the posted body at `:83`. Both now anchor
47
+ correctly.
48
+ 2. **Server-side DCR read as client-side.** `AI-Employee`'s registration
49
+ *endpoint* — which receives and stores an incoming client — was flagged
50
+ under rules that constrain clients.
51
+ 3. **Request-body population read as a credential store.**
52
+ `data["client_secret"] = self.client_secret` at `ogx…:150`.
53
+
54
+ `ogx-ai/ogx` is the first real-world **`AUTH_ISS_UNVALIDATED` true positive**:
55
+ a hand-rolled `requests.post(token_endpoint, data={"grant_type":
56
+ "authorization_code", ...})` with no `iss` or `issuer` token anywhere in the
57
+ file — precisely the SEP-2468 MUST. The other two clients are `iss`-aware (12
58
+ and 9 mentions) and correctly stay clean.
59
+
60
+ Sample size is three files, so this is not a precision *rate* — it is evidence
61
+ that the rules work on the population they target, and that the SDK-heavy
62
+ corpus was systematically blind to a whole class of defect. Both wild false
63
+ positives are now regression fixtures (`negatives/server_dcr_handler.py`,
64
+ `auth/handrolled-client.py`).
65
+
66
+ ## Results — v0.10.4 (22-rule engine, HTTP+SSE transport rule added)
67
+
68
+ Re-run 2026-08-09 with `mcp-vet` v0.10.4 on the SAME pinned corpus:
69
+ **258 findings across 68 files** — the 243 findings of the v0.10.2/0.10.3 run
70
+ plus **15 new `SSE_TRANSPORT_DEPRECATED` findings**. A byte-for-byte diff of
71
+ the non-SSE findings against the 0.10.3 run is **identical** — zero drift in
72
+ the other 21 rules.
73
+
74
+ Every new finding, hand-labeled against source (all 15 are true positives, 0
75
+ false positives):
76
+
77
+ | File | Findings | Label | What it is |
78
+ | --- | ---: | --- | --- |
79
+ | typescript-sdk `everything/transports/sse.ts` | 7 | **TP ×7** | the SDK's own legacy SSE transport example: `SSEServerTransport` import from `@modelcontextprotocol/sdk/server/sse.js` (class + module path on the import line) and five usage sites (type annotations, `transports.get(...) as SSEServerTransport`, `new SSEServerTransport("/message", res)`) |
80
+ | typescript-sdk `guides/clients/connect.examples.ts` | 2 | **TP ×2** | `SSEClientTransport` imported and passed to `client.connect(new SSEClientTransport(new URL(url)))` |
81
+ | python-sdk `clients/simple-auth-client/…/main.py` | 3 | **TP ×3** | `from mcp.client.sse import sse_client` (module path + helper name) and the `async with sse_client(...)` usage |
82
+ | python-sdk `snippets/clients/url_elicitation_client.py` | 3 | **TP ×3** | same shape: `from mcp.client.sse import sse_client` + usage |
83
+
84
+ Two findings each on the two Python import lines and the two TS import lines
85
+ are the documented column-level (not line-level) dedup: the module path and
86
+ the imported symbol are separate signals at separate columns.
87
+
88
+ So the v0.10.4 headline on this corpus is **256/258 true positives (2 FP,
89
+ 0.8%)** — the two FPs are the pre-existing v0.9.0 pair, untouched. The
90
+ hand-rolled two-endpoint shape (`text/event-stream` + an `event: endpoint`
91
+ write) and the `transport="sse"` literal contribute zero corpus findings —
92
+ correctly, since the corpus's SSE usage is all SDK-routed — and their positive
93
+ behaviour is proven by `test/fixtures/sse/` instead (`handrolled-sse.ts`,
94
+ `fastmcp_sse.py`), with `negatives/streamable-http-server.ts` /
95
+ `negatives/streamable_http_server.py` / `negatives/plain_sse_feed.py` locking
96
+ that `text/event-stream` alone — which every correct Streamable HTTP server
97
+ contains — never fires.
98
+
99
+ ## Results — v0.10.2 (21-rule engine, auth-hardening rules added)
100
+
101
+ Re-run 2026-08-01 with `mcp-vet` v0.10.2 on the SAME pinned corpus:
102
+ **243 findings across 66 files** — identical to the v0.9.0 run. By confidence:
103
+ 100 high, 142 medium, 1 low.
104
+
105
+ | New pattern | Findings |
106
+ | --- | --- |
107
+ | `AUTH_ISS_UNVALIDATED` | 0 |
108
+ | `AUTH_DCR_NO_APPLICATION_TYPE` | 0 |
109
+ | `AUTH_CREDENTIALS_NOT_ISSUER_KEYED` | 0 |
110
+
111
+ **The three auth-hardening rules produce zero findings on this corpus, and that
112
+ is the correct result** — not a sign they do nothing. The official SDK examples
113
+ are already compliant: both SDKs supply `application_type`, the example clients
114
+ read `iss` from the callback, and the one apparent credential-store hit was an
115
+ authorization-server token cache. The rules are deliberately conservative, and
116
+ their positive behaviour is proven by fixtures instead — `test/fixtures/auth/`
117
+ (a hand-rolled registration + un-validated redemption → exactly 2 findings) and
118
+ `test/fixtures/dirty/` (all three, in both analyzers). A corpus of correct code
119
+ *should* be quiet; the fixtures are what stop that quiet from being vacuous.
120
+
121
+ > **Correction — v0.10.0 overstated this table.** 0.10.0 reported 247 findings
122
+ > and labeled three `AUTH_DCR_NO_APPLICATION_TYPE` hits in the python-sdk
123
+ > examples as *true* positives. They were false positives, and the rule was
124
+ > wrong, not just the label: both official SDKs supply `application_type` for
125
+ > you. python-sdk `src/mcp/shared/auth.py` defaults it on
126
+ > `OAuthClientMetadata` (*"SEP-837: OIDC application_type. Defaults to
127
+ > `"native"`…"*), and typescript-sdk
128
+ > `packages/client/src/client/auth.ts:902` derives it
129
+ > (`clientMetadata.application_type ?? deriveApplicationType(clientMetadata.redirect_uris)`).
130
+ > A body routed through either is already correct. 0.10.1 recognizes those SDK
131
+ > symbols and only flags a **hand-rolled** registration POST; the three corpus
132
+ > findings are gone, and the shapes are locked as true negatives
133
+ > (`negatives/sdk_dcr_defaults.py`, `negatives/sdk-dcr-defaults.ts`). The
134
+ > headline below is the corrected measurement.
135
+
136
+ Notes on the two rules that could have fired and correctly did not:
137
+
138
+ - `AUTH_CREDENTIALS_NOT_ISSUER_KEYED` flagged
139
+ `servers/simple-auth/mcp_simple_auth/simple_auth_provider.py:217` in 0.10.0
140
+ and 0.10.1 — `self.tokens[mcp_token] = AccessToken(…, client_id=…)`, an
141
+ *authorization-server access-token cache* keyed by the token it just minted,
142
+ not a client persisting registration credentials. It was counted as a false
143
+ positive in both releases; **0.10.2 fixes the rule** (a token-shaped
144
+ container or value is exempt unless the stored value carries a
145
+ `client_secret` — only registration hands one out). Locked by
146
+ `negatives/server_token_cache.py`.
147
+ - `AUTH_ISS_UNVALIDATED` produced **zero** corpus findings — correctly. The
148
+ SDK example clients either read `iss` from the callback (`main.py`,
149
+ `oauth_client.py` — the rule sees the token and stays quiet) or drive the
150
+ exchange through SDK helpers with no `grant_type`/`authorization_code`
151
+ literal in the file, which is the documented
152
+ `adversarial/missed/auth-helper-indirection.ts` recall boundary.
153
+
154
+ So the v0.10.2 headline on this corpus is **241/243 true positives (2 FP,
155
+ 0.8%)** — exactly the two v0.9.0 FPs, both unrelated to the new rules. The
156
+ auth-hardening rules added no false positives and no true positives here; the
157
+ precision improvement over 0.10.0's *reported* 1.2% is the removal of three
158
+ mislabeled findings and one genuine FP, not a change to the older rules.
159
+
160
+ ## Results — v0.9.0 (18-rule engine, final 2026-07-28 changelog)
161
+
162
+ Re-run 2026-07-28 with `mcp-vet` v0.9.0 on the SAME pinned corpus:
163
+ **243 findings across 66 files** (was 105/41 with the 9-rule engine — the
164
+ final changelog's removals fire heavily on the SDKs' own pre-final examples).
165
+ By confidence: 100 high, 142 medium, 1 low.
166
+
167
+ | Pattern | Findings |
168
+ | --- | --- |
169
+ | `SSE_RESUMABILITY_REMOVED` | 65 |
170
+ | `MCP_SESSION_ID` | 49 |
171
+ | `ELICITATION_COMPLETE_REMOVED` | 44 |
172
+ | `RESOURCE_SUBSCRIBE_REMOVED` | 23 |
173
+ | `SAMPLING_CAP` | 16 |
174
+ | `LOGGING_CAP` | 16 |
175
+ | `ROOTS_CAP` | 10 |
176
+ | `ROOTS_LIST_CHANGED_REMOVED` | 5 |
177
+ | `INITIALIZE_HANDLER` | 4 |
178
+ | `PING_REMOVED` | 3 |
179
+ | `TASKS_LEGACY` | 2 |
180
+ | `TASKS_RESULT_REMOVED` | 2 |
181
+ | `OAUTH_DCR` | 2 |
182
+ | `ERROR_CODE_RENUMBERED` | 1 |
183
+ | `LOGGING_SETLEVEL_REMOVED` | 1 |
184
+
185
+ Labeling of the 138 NEW findings (spot-reviewed per category against source):
186
+
187
+ - **136 true positives.** The corpus repos genuinely implement the removed
188
+ surfaces: the typescript-sdk `everything` example ships an
189
+ `InMemoryEventStore` + `Last-Event-ID` resumability transport (all 65 SSE
190
+ findings sit in those transport/resumability files), the elicitation examples
191
+ register `notifications/elicitation/complete` handlers and read
192
+ `elicitationId`, `everything/resources/subscriptions.ts` and friends register
193
+ `SubscribeRequestSchema`/`UnsubscribeRequestSchema`, and the bearer-auth
194
+ clients send `method: 'ping'`.
195
+ - **2 counted as false positives (honest reading):**
196
+ `guides/serving/sessions-state-scaling.examples.ts:62` returns
197
+ `code: -32001` for "Session not found" — an *implementation-defined* use the
198
+ final policy grandfathers; static analysis cannot distinguish it from the
199
+ renumbered `HeaderMismatch`, so ERROR_CODE_RENUMBERED flags it and we count
200
+ it against ourselves. Plus the pre-existing `mcp-session-id` negative
201
+ assertion (below). During this run a `registerTool('ping', ...)` false
202
+ positive (a tool merely NAMED ping) was found and FIXED before release —
203
+ strict registration context now excludes tool/prompt/resource registration
204
+ calls, locked into `negatives/`.
205
+
206
+ So the v0.9.0 headline on this corpus is **241/243 true positives (2 FP,
207
+ 0.8%)** — same discipline as before: FPs are counted, not defined away.
208
+
209
+ ## Results — v0.4.0 (9-rule engine, release-candidate era)
210
+
211
+ **105 findings across 41 files** (TypeScript/JavaScript: 93, Python: 12).
212
+ By confidence: 66 high, 38 medium, 1 low.
213
+
214
+ | Pattern | Findings |
215
+ | --- | --- |
216
+ | `MCP_SESSION_ID` | 49 |
217
+ | `LOGGING_CAP` | 17 |
218
+ | `SAMPLING_CAP` | 16 |
219
+ | `ROOTS_CAP` | 15 |
220
+ | `INITIALIZE_HANDLER` | 4 |
221
+ | `TASKS_LEGACY` | 2 |
222
+ | `TASKS_RESULT_REMOVED` | 2 |
223
+
224
+ ### Labeling
225
+
226
+ Every finding was manually reviewed against its source line:
227
+
228
+ - **104 / 105 true positives** — real references to a removed or deprecated
229
+ protocol surface (session headers/ids, handshake registration, legacy task
230
+ methods, deprecated capability declarations and method strings).
231
+ - **1 / 105 false positive (0.95%)** —
232
+ `stories/json_response/client.py:62` in the typescript-sdk examples:
233
+ `assert "mcp-session-id" not in response.headers`. That line is
234
+ *already-migrated* test code asserting the header is **absent**; flagging it
235
+ as "will break" is wrong. It is exactly what inline suppression
236
+ (`# mcp-vet-disable-line MCP_SESSION_ID`) is for, but we count it as a false
237
+ positive rather than defining it away. So the honest headline is
238
+ **"1 false positive in 44k LOC"**, not zero.
239
+
240
+ Notes on reading the numbers:
241
+
242
+ - Two occurrences on one line (e.g. `transport.sessionId && sessions.delete(transport.sessionId)`)
243
+ are reported as two findings — column-level dedup, not line-level.
244
+ - Findings in test files (`__tests__/…`) are counted as true positives: a test
245
+ that registers `sampling/createMessage` breaks the same way production code
246
+ does.
247
+
248
+ ### Labeled negatives
249
+
250
+ Files asserted to stay **clean** are part of the repo's test suite and run in CI:
251
+
252
+ - `test/fixtures/clean/` — a full server written in the 2026-07-28 style
253
+ (per-request `_meta`, `sessionIdGenerator: undefined`, `-32602`).
254
+ - `test/fixtures/negatives/` — "false friend" patterns: `sessionId` on plain
255
+ app-level objects, `-32002` inside strings/comments, capability-like words
256
+ with no capabilities context.
257
+ - `test/fixtures/adversarial/caught/` — obfuscations the scanner **must**
258
+ catch: aliased imports (TS + Python), namespace-qualified SDK constants,
259
+ client transports resuming a `sessionId`.
260
+
261
+ Additionally, in the corpus above, comment-only mentions (e.g. `Mcp-Session-Id`
262
+ in a comment, `initialize` in prose) produced zero findings — the AST layer
263
+ distinguishes executable tokens from comments by construction.
264
+
265
+ ## Recall — what the scanner is known to miss
266
+
267
+ Static token analysis proves known patterns are **absent**; it cannot prove
268
+ your server **speaks the new wire contract**. Recall is bounded by
269
+ construction, and the misses are locked into the test suite
270
+ (`test/fixtures/adversarial/missed/`, asserted to produce zero findings so any
271
+ silent claim-inflation fails CI):
272
+
273
+ - split/computed method strings — `'tasks' + '/list'`, `` `tasks/${op}` ``, f-strings
274
+ - computed capability keys — `{ ['roo'+'ts']: {} }`
275
+ - generated/loop-driven registration from string fragments
276
+ - framework-adapter indirection (route tables built at runtime)
277
+ - cross-module renames — a wrapper re-exporting an SDK constant under a new
278
+ name is flagged in the wrapper file, but a consumer importing only the new
279
+ name scans clean on its own
280
+
281
+ There is no corpus-wide recall *percentage*: that would require a labeled set
282
+ of every legacy usage in the wild, which nobody has. What we can say is: for
283
+ the pattern shapes listed in the README, detection is exact; for the shapes
284
+ above, it is zero, and the tool says so — pair the scan with runtime checks
285
+ (`mcp-vet fixtures`) to cover the difference.
286
+
287
+ ## Reproducing
288
+
289
+ ```bash
290
+ git clone --depth 1 https://github.com/modelcontextprotocol/servers
291
+ git clone --depth 1 https://github.com/modelcontextprotocol/typescript-sdk
292
+ git clone --depth 1 https://github.com/modelcontextprotocol/python-sdk
293
+ # check out the pinned SHAs above, then:
294
+ npx @booyaka/mcp-vet servers/src typescript-sdk/examples python-sdk/examples --json --no-files
295
+ ```