@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 +295 -222
- package/CHANGELOG.md +97 -0
- package/README.md +10 -7
- package/dist/cli.js +1 -1
- package/dist/probe-cli.js +1 -1
- package/dist/probe.js +97 -1
- package/dist/py-fallback.js +25 -0
- package/dist/python/mcp_ast_scan.py +28 -0
- package/dist/rules.js +143 -9
- package/dist/ts-analyzer.js +25 -2
- package/dist/types.d.ts +16 -2
- package/dist/types.js +2 -0
- package/package.json +73 -71
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
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
|
106
|
-
|
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
+
```
|