@booyaka/mcp-vet 0.4.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 +81 -0
- package/README.md +64 -4
- package/dist/autofix.d.ts +2 -2
- package/dist/autofix.js +2 -2
- package/dist/cli.js +198 -178
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +4 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +12 -1
- package/dist/probe-cli.d.ts +1 -0
- package/dist/probe-cli.js +145 -0
- package/dist/probe.d.ts +37 -0
- package/dist/probe.js +588 -0
- package/dist/reporters.d.ts +9 -6
- package/dist/reporters.js +56 -4
- package/dist/rules.d.ts +15 -1
- package/dist/rules.js +39 -1
- package/dist/schema-dialect.d.ts +29 -0
- package/dist/schema-dialect.js +115 -0
- package/dist/suppress.d.ts +2 -2
- package/dist/types.d.ts +19 -2
- package/dist/types.js +8 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,87 @@ All notable changes to `mcp-vet` are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
|
|
5
5
|
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.6.0]
|
|
8
|
+
|
|
9
|
+
The full 2026-07-28 compliance suite — `mcp-vet probe --spec-version 2026-07-28`
|
|
10
|
+
now covers all three breaking changes the release candidate makes to server
|
|
11
|
+
behavior, not just the removed handshake.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`missing-server-discover` (ERROR, `--spec-version 2026-07-28`)** — calls
|
|
16
|
+
the `server/discover` RPC that every 2026-07-28 server MUST implement
|
|
17
|
+
(SEP-2575; it replaces the removed initialize handshake for up-front
|
|
18
|
+
capability discovery) and flags a server whose answer is an error, a hang, or
|
|
19
|
+
a result missing the required `capabilities` key. The spec defines
|
|
20
|
+
`server/discover` as JSON-RPC only — 2026-07-28 removes the HTTP GET
|
|
21
|
+
endpoint, so no `GET /mcp/discover` variant is probed.
|
|
22
|
+
- **`legacy-resource-error-code` (ERROR, `--spec-version 2026-07-28`)** — reads
|
|
23
|
+
a deliberately nonexistent resource URI (`mcp-vet://probe/...`) and flags a
|
|
24
|
+
server that still answers `-32002` instead of the JSON-RPC standard `-32602`
|
|
25
|
+
(Invalid Params). Servers without `resources/read` (`-32601`) are skipped,
|
|
26
|
+
not flagged; unexpected codes are reported as inconclusive notes.
|
|
27
|
+
- **`mcp-vet run`** — an alias for `mcp-vet probe`.
|
|
28
|
+
- **`test/probe-fixtures/server-partial.mjs`** — a hybrid (handshake +
|
|
29
|
+
stateless) fixture with exactly one migration defect per mode
|
|
30
|
+
(`legacy-error-code` / `no-discover` / `bad-discover`), isolating each new
|
|
31
|
+
rule; 7 new tests (62 total), including proof that the new checks are gated
|
|
32
|
+
behind `--spec-version 2026-07-28` and the default probe is unchanged.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- The stateless first request now sends the RC's exact namespaced `_meta` key
|
|
37
|
+
`io.modelcontextprotocol/clientCapabilities` (was the incorrect
|
|
38
|
+
`io.modelcontextprotocol/capabilities`); the stateless fixtures now *require*
|
|
39
|
+
the namespaced keys, locking the wire format into the tests.
|
|
40
|
+
- The new checks run on whichever contact path succeeded (stateless or classic
|
|
41
|
+
fallback), so a handshake-only legacy server gets its complete 2026-07-28
|
|
42
|
+
migration report — handshake + discover findings — in a single probe.
|
|
43
|
+
- `ProbeResult` gains `discoverOk` and `errorCodeOk` verdict fields.
|
|
44
|
+
|
|
45
|
+
## [0.5.0]
|
|
46
|
+
|
|
47
|
+
The runtime-probe release — `mcp-vet probe` connects to a *running* MCP server
|
|
48
|
+
(stdio command or Streamable HTTP URL) and detects the two 2026-07-28 violation
|
|
49
|
+
categories that only exist on the wire, not in source.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- **`mcp-vet probe [options] <url | command...>`** — a runtime prober with two
|
|
54
|
+
new violation categories, reported in the same JSON + SARIF formats as the
|
|
55
|
+
static scan:
|
|
56
|
+
- **`json-schema-dialect` (WARN)** — calls `tools/list` and inspects every
|
|
57
|
+
tool's `inputSchema`/`outputSchema` (SEP-2106 lifts both to full JSON
|
|
58
|
+
Schema 2020-12). Flags an explicit draft-04/-06/-07 (or 2019-09) `$schema`
|
|
59
|
+
at high confidence, and — when `$schema` is absent — draft-only keyword
|
|
60
|
+
forms (`definitions`, `$ref: "#/definitions/…"`, boolean
|
|
61
|
+
`exclusiveMinimum`/`exclusiveMaximum`, array-form `items`, schema-form
|
|
62
|
+
`dependencies`) at medium confidence. The walker recurses only into schema
|
|
63
|
+
positions, so a *property* named `definitions` is never a false positive,
|
|
64
|
+
and an explicit 2020-12 `$schema` is trusted.
|
|
65
|
+
- **`requires-initialize-handshake` (ERROR)** — with
|
|
66
|
+
`--spec-version 2026-07-28`, makes a stateless first request (no
|
|
67
|
+
`initialize`; protocolVersion/clientInfo/capabilities travel in `_meta`
|
|
68
|
+
per the RC) and flags a server that rejects it or hangs. Cross-checked:
|
|
69
|
+
only emitted when the classic 2025-11-25 handshake path *does* work, so a
|
|
70
|
+
dead server is an operational error (exit 2), never a false violation.
|
|
71
|
+
- **`--spec-version <2025-11-25|2026-07-28>`** (default `2025-11-25`) selects
|
|
72
|
+
the revision to vet against; `--timeout <ms>` bounds each request and doubles
|
|
73
|
+
as the hang-detection window; `--json` / `--sarif [file]` / `--fail-on` /
|
|
74
|
+
`--quiet` / `--color` work as in the scan.
|
|
75
|
+
- **Runtime rules in SARIF** — probe rules join the driver metadata when they
|
|
76
|
+
fire (`ERROR` → `error`, `WARN` → `warning`); the static-scan SARIF keeps its
|
|
77
|
+
stable 9-rule shape.
|
|
78
|
+
- **`test/probe-fixtures/`** — minimal real MCP servers used by 18 new tests:
|
|
79
|
+
`server-draft07.mjs` (explicit + inferable draft-07 tools, a modern 2020-12
|
|
80
|
+
tool, and a property literally named `definitions`), `server-requires-init.mjs`
|
|
81
|
+
(rejects pre-initialize requests with `-32002`), `server-stateless.mjs`
|
|
82
|
+
(2026-07-28-native, requires `_meta`, no initialize), and `server-http.mjs`
|
|
83
|
+
(Streamable HTTP, sessionful *and* stateless modes).
|
|
84
|
+
- Verified against the official `@modelcontextprotocol/server-everything@2026.7.4`:
|
|
85
|
+
it answers stateless requests, but all of its tool schemas still declare
|
|
86
|
+
draft-07 — `probe` reports 14 true `json-schema-dialect` findings.
|
|
87
|
+
|
|
7
88
|
## [0.4.0]
|
|
8
89
|
|
|
9
90
|
The community-feedback release — everything in it traces to reader comments on
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ npx @booyaka/mcp-vet .
|
|
|
18
18
|
<img src="https://raw.githubusercontent.com/Booyaka101/mcp-vet/main/assets/demo.png" alt="mcp-vet scanning a server — BREAKING and DEPRECATED findings with before/after fixes and confidence tags" width="720">
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
|
-
No account, no API key
|
|
21
|
+
No account, no API key — the scan parses your code locally (ts-morph for TS/JS, a bundled Python `ast` script for `.py`), makes no network calls, and exits non-zero if it finds anything **BREAKING**, so you can drop it straight into CI. (The opt-in [`mcp-vet probe`](#vet-a-running-server-mcp-vet-probe) is the one command that talks to a server — and only the one you point it at.)
|
|
22
22
|
|
|
23
23
|
## What actually happens on July 28
|
|
24
24
|
|
|
@@ -187,7 +187,7 @@ case 'tasks/list': return listTasks();
|
|
|
187
187
|
- **The long-lived server→client SSE push channel is removed** — a server may only send requests to the client *while it is actively processing a client request*. Standing push streams / out-of-band notifications need rework.
|
|
188
188
|
- **Streamable HTTP now requires `Mcp-Method` and `Mcp-Name` headers** that mirror the JSON-RPC body; servers must reject requests where headers and body disagree.
|
|
189
189
|
- **Auth hardening** — validate the RFC 9207 `iss` parameter, declare OIDC `application_type` on Dynamic Client Registration, and bind tokens to the issuing authorization server.
|
|
190
|
-
- **Tool schemas may now be full JSON Schema 2020-12** (`oneOf`/`anyOf`/`$ref`/conditionals); do not auto-dereference external `$ref` URIs.
|
|
190
|
+
- **Tool schemas may now be full JSON Schema 2020-12** (`oneOf`/`anyOf`/`$ref`/conditionals); do not auto-dereference external `$ref` URIs. The *dialect* half of this — schemas still declaring or using draft-07 forms — **is** detectable at runtime: [`mcp-vet probe`](#vet-a-running-server-mcp-vet-probe) checks it against your live server.
|
|
191
191
|
|
|
192
192
|
The CLI prints a one-line reminder of these after every scan.
|
|
193
193
|
|
|
@@ -213,6 +213,65 @@ writes nine ready-to-fire JSON fixtures plus a `CHECKLIST.md`, covering the runt
|
|
|
213
213
|
|
|
214
214
|
Each fixture is a plain JSON description (`send` headers + JSON-RPC body, `expect` notes) you can replay with curl, supertest, pytest + httpx, or any HTTP harness. The checklist also spells out the **dual-version rollout matrix** — run both `2025-11-25` and `2026-07-28` paths until your clients have all moved — and a **client-side assumptions** list (session resume, per-request `_meta`, retries landing on other instances, `tools/list` revalidation).
|
|
215
215
|
|
|
216
|
+
## Vet a running server (`mcp-vet probe`)
|
|
217
|
+
|
|
218
|
+
Where the scan reads your *source*, `probe` talks to your *running server* over the wire — stdio (a command it spawns) or Streamable HTTP (a URL) — and checks the 2026-07-28 violations that only exist at runtime (`run` is an alias: `mcp-vet run …` ≡ `mcp-vet probe …`):
|
|
219
|
+
|
|
220
|
+
| ID | Severity | What it checks |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `json-schema-dialect` | 🟡 WARN | calls `tools/list` and inspects every tool's `inputSchema`/`outputSchema` for a pre-2020-12 JSON Schema dialect ([SEP-2106](https://modelcontextprotocol.io/seps/2106-json-schema-2020-12)) — an explicit draft-04/-06/-07 `$schema` (**high** confidence), or no `$schema` but draft-only keyword forms: `definitions` instead of `$defs`, `$ref: "#/definitions/…"`, boolean `exclusiveMinimum`/`exclusiveMaximum`, array-form `items` (**medium** confidence) |
|
|
223
|
+
| `requires-initialize-handshake` | 🔴 ERROR | with `--spec-version 2026-07-28`: makes a **stateless first request** — no `initialize`, protocolVersion/clientInfo/clientCapabilities in namespaced `_meta` keys per the RC — and flags a server that rejects it or hangs waiting for the removed handshake. A valid `tools` array in the answer is asserted, not just a 200 |
|
|
224
|
+
| `missing-server-discover` | 🔴 ERROR | with `--spec-version 2026-07-28`: calls the **`server/discover`** RPC that every 2026-07-28 server MUST implement ([SEP-2575](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575) — it replaces the handshake for up-front capability discovery) and flags a server whose answer is an error or lacks the required `capabilities` key. (The spec defines `server/discover` as JSON-RPC only — 2026-07-28 *removes* the HTTP GET endpoint, so there is no `GET /mcp/discover` to fall back to) |
|
|
225
|
+
| `legacy-resource-error-code` | 🔴 ERROR | with `--spec-version 2026-07-28`: reads a deliberately nonexistent resource URI and flags a server that still answers with the MCP-custom **`-32002`** instead of the JSON-RPC standard **`-32602`** (Invalid Params). Servers without `resources/read` (`-32601`) are skipped, not flagged |
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# vet the schemas of a stdio server (spawns the command; a lone .js file runs with Node)
|
|
229
|
+
npx @booyaka/mcp-vet probe node ./dist/server.js
|
|
230
|
+
|
|
231
|
+
# full 2026-07-28 readiness: stateless first contact + server/discover +
|
|
232
|
+
# resource error code + schema dialects
|
|
233
|
+
npx @booyaka/mcp-vet probe --spec-version 2026-07-28 http://localhost:3000/mcp
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```text
|
|
237
|
+
mcp-vet probe — node ./dist/server.js · spec 2026-07-28 · stdio · 12 tool(s) listed
|
|
238
|
+
stateless probe: stateless tools/list was rejected: -32002 Server not initialized
|
|
239
|
+
fallback probe: initialize handshake + tools/list succeeded
|
|
240
|
+
server/discover: rejected (-32601)
|
|
241
|
+
resource error-code check skipped — server does not implement resources/read (-32601)
|
|
242
|
+
|
|
243
|
+
ERROR requires-initialize-handshake [high]
|
|
244
|
+
The server rejected (or hung on) a stateless 2026-07-28-style first request ...
|
|
245
|
+
ERROR missing-server-discover [high]
|
|
246
|
+
The 2026-07-28 spec requires every server to implement the server/discover RPC ...
|
|
247
|
+
WARN json-schema-dialect [high]
|
|
248
|
+
tool "echo" inputSchema: $schema = http://json-schema.org/draft-07/schema# (draft-07)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The stateless verdict is **cross-checked** before it becomes a violation: `requires-initialize-handshake` is only emitted when the classic `2025-11-25` handshake path *does* work — a dead or non-MCP server is an operational error (exit 2), never a false violation. The `server/discover` and error-code checks then run on whichever contact path succeeded, so even a handshake-only server gets its complete migration report in one probe. The dialect walker recurses only into schema positions (applicators like `properties`/`allOf`), so a *property* literally named `definitions` is never mistaken for the draft-07 keyword, and an explicit 2020-12 `$schema` declaration is trusted.
|
|
252
|
+
|
|
253
|
+
### `--spec-version` — which revision to vet against
|
|
254
|
+
|
|
255
|
+
| Value | Behavior |
|
|
256
|
+
| --- | --- |
|
|
257
|
+
| `2025-11-25` *(default)* | today's stable contract: classic `initialize` handshake, then the `json-schema-dialect` check. **No 2026-07-28 assertions run** — a fully 2025-era server probes clean |
|
|
258
|
+
| `2026-07-28` | the full new-spec compliance suite: stateless first contact, required `server/discover`, `-32602` resource error code, plus the dialect check |
|
|
259
|
+
|
|
260
|
+
**Migration note.** The default stays `2025-11-25` so existing CI invocations keep their exact behavior — add the flag when *you* are ready, not when the spec ships. A practical rollout:
|
|
261
|
+
|
|
262
|
+
1. Today: `mcp-vet probe <server>` (unchanged) plus the static scan in CI.
|
|
263
|
+
2. When you start migrating: add a second CI job with `--spec-version 2026-07-28 --fail-on none` to *see* the new-spec violations without failing the build.
|
|
264
|
+
3. When your server targets `2026-07-28` (e.g. after moving to `@modelcontextprotocol/server` 2.x): drop `--fail-on none` so the three ERROR-level checks gate the build. A correctly migrated server passes all of them; the pre-migration server fails `requires-initialize-handshake` and `missing-server-discover` immediately.
|
|
265
|
+
4. Keep a `2025-11-25` probe in the matrix until every client you serve has moved (the rollout is a window, not a day — see [What actually happens on July 28](#what-actually-happens-on-july-28)).
|
|
266
|
+
|
|
267
|
+
Probe findings use the same report formats as the scan: `--json` (machine-readable array on stdout) and `--sarif [file]` (SARIF 2.1.0 — `ERROR` maps to `error`, `WARN` to `warning`), plus `--fail-on breaking|any|none` (default `breaking`: exit 1 only on `ERROR`), `--timeout <ms>` (default 8000, also the hang-detection window), `--quiet`, and `--color`/`--no-color`.
|
|
268
|
+
|
|
269
|
+
Try it against the official reference server — the July 2026 `@modelcontextprotocol/server-everything` (beta 2026-07-28 SDK) answers stateless requests and already returns the new `-32602` resource error code, but it does not implement `server/discover` yet and its tool schemas still declare draft-07 — `probe` reports exactly that (1 ERROR, 13 WARN):
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npx @booyaka/mcp-vet probe --spec-version 2026-07-28 npx -y @modelcontextprotocol/server-everything stdio
|
|
273
|
+
```
|
|
274
|
+
|
|
216
275
|
## Usage
|
|
217
276
|
|
|
218
277
|
```bash
|
|
@@ -221,6 +280,7 @@ npx @booyaka/mcp-vet . --fix # scan, and auto-apply the mechanical -32
|
|
|
221
280
|
npx @booyaka/mcp-vet ./src ./packages # multiple roots
|
|
222
281
|
npx @booyaka/mcp-vet server.py # a single file
|
|
223
282
|
npx @booyaka/mcp-vet fixtures ./dir # write runtime conformance fixtures + checklist (default: ./mcp-vet-fixtures)
|
|
283
|
+
npx @booyaka/mcp-vet probe <url|cmd> # vet a RUNNING server's wire behavior (see section above; alias: run)
|
|
224
284
|
```
|
|
225
285
|
|
|
226
286
|
Globs `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and `**/*.py`, skipping `node_modules`, `.git`, `__pycache__`, `dist`, and `build`.
|
|
@@ -426,10 +486,10 @@ Also exported: `renderJson` / `renderMarkdown` / `renderSarif`, `RULES`, and the
|
|
|
426
486
|
```bash
|
|
427
487
|
npm install # installs deps and builds (via prepare)
|
|
428
488
|
npm run build # tsc -> dist/ + copies the Python script
|
|
429
|
-
npm test # builds, then runs the Node.js built-in test runner (
|
|
489
|
+
npm test # builds, then runs the Node.js built-in test runner (62 tests)
|
|
430
490
|
```
|
|
431
491
|
|
|
432
|
-
Test fixtures live in `test/fixtures/` (dirty TS + Python servers, a `clean/` server with zero violations, `negatives/` true-negatives, a `confidence/` gradient, and `suppress/` cases).
|
|
492
|
+
Test fixtures live in `test/fixtures/` (dirty TS + Python servers, a `clean/` server with zero violations, `negatives/` true-negatives, a `confidence/` gradient, and `suppress/` cases). Runtime-probe fixtures live in `test/probe-fixtures/` — minimal stdio + Streamable-HTTP MCP servers: one returning draft-07 schemas, one requiring the initialize handshake, one fully migrated 2026-07-28-native (stateless + `server/discover` + `-32602`), and a `server-partial.mjs` with one deliberate migration defect per mode (`legacy-error-code` / `no-discover` / `bad-discover`).
|
|
433
493
|
|
|
434
494
|
## License
|
|
435
495
|
|
package/dist/autofix.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Finding,
|
|
1
|
+
import { Finding, ViolationId } from './types';
|
|
2
2
|
export interface FixPreview {
|
|
3
3
|
file: string;
|
|
4
4
|
line: number;
|
|
@@ -16,7 +16,7 @@ export interface FixOptions {
|
|
|
16
16
|
/** Compute and return the rewrites without touching any files. */
|
|
17
17
|
dryRun?: boolean;
|
|
18
18
|
}
|
|
19
|
-
export declare function isFixable(id:
|
|
19
|
+
export declare function isFixable(id: ViolationId): boolean;
|
|
20
20
|
/**
|
|
21
21
|
* Apply the safe mechanical fixes in place. Returns which findings were fixed so
|
|
22
22
|
* the caller can drop them from the report and the exit-code calculation.
|
package/dist/autofix.js
CHANGED
|
@@ -110,9 +110,9 @@ function applyFixes(findings, opts = {}) {
|
|
|
110
110
|
preview.push(...localPreview);
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
|
-
// Only count/return findings as fixed once the write actually succeeds
|
|
113
|
+
// Only count/return findings as fixed once the write actually succeeds — a
|
|
114
114
|
// failed write (read-only file, EACCES) must not report the code as fixed.
|
|
115
|
-
const out = (hasBom ? '
|
|
115
|
+
const out = (hasBom ? '' : '') + lines.join('\n');
|
|
116
116
|
try {
|
|
117
117
|
fs.writeFileSync(absPath, out, 'utf8');
|
|
118
118
|
}
|
package/dist/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ const constants_1 = require("./constants");
|
|
|
45
45
|
const reporters_1 = require("./reporters");
|
|
46
46
|
const autofix_1 = require("./autofix");
|
|
47
47
|
const conformance_1 = require("./conformance");
|
|
48
|
+
const probe_cli_1 = require("./probe-cli");
|
|
48
49
|
const CONF_VALUES = ['high', 'medium', 'low'];
|
|
49
50
|
const FAILON_VALUES = ['breaking', 'any', 'none'];
|
|
50
51
|
function fail(msg) {
|
|
@@ -97,199 +98,218 @@ if (process.argv[2] === 'fixtures') {
|
|
|
97
98
|
fail(`could not write fixtures: ${err.message}`);
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.option('--sarif [file]', 'write a SARIF 2.1.0 report (default file: mcp-vet.sarif)')
|
|
107
|
-
.option('--out-dir <dir>', 'directory for mcp-vet-report.md and mcp-vet-results.json', process.cwd())
|
|
108
|
-
.option('--no-files', 'do not write the markdown/json report files')
|
|
109
|
-
.option('--only <ids>', 'only run these pattern ids (comma/space separated)')
|
|
110
|
-
.option('--disable <ids>', 'skip these pattern ids (comma/space separated)')
|
|
111
|
-
.option('--fail-on <level>', `exit non-zero on: ${FAILON_VALUES.join(' | ')}`, 'breaking')
|
|
112
|
-
.option('--min-confidence <level>', `report only findings at/above: ${CONF_VALUES.join(' | ')}`, 'low')
|
|
113
|
-
.option('--ignore <glob>', 'ignore paths matching glob (repeatable)', (v, acc) => {
|
|
114
|
-
acc.push(v);
|
|
115
|
-
return acc;
|
|
116
|
-
}, [])
|
|
117
|
-
.option('--max-file-size <kb>', 'skip files larger than this many KB (0 = no limit)', '1536')
|
|
118
|
-
.option('--no-py-fallback', 'disable the regex fallback when no Python interpreter is found')
|
|
119
|
-
.option('--config <path>', 'path to a config file (.mcpvetrc.json)')
|
|
120
|
-
.option('--fix', 'auto-apply the safe mechanical fixes in place (currently: -32002 → -32602)')
|
|
121
|
-
.option('--dry-run', 'with --fix: print the rewrites that would be made, without changing files')
|
|
122
|
-
.option('--json', 'print findings as a JSON array to stdout (implies a quiet terminal report)')
|
|
123
|
-
.option('--color', 'force colored output')
|
|
124
|
-
.option('--no-color', 'disable colored output')
|
|
125
|
-
.option('--quiet', 'suppress the human-readable terminal report')
|
|
126
|
-
.version((0, constants_1.getVersion)(), '-v, --version')
|
|
127
|
-
.addHelpText('after', '\nCommands:\n fixtures [dir] write protocol-level conformance fixtures + CHECKLIST.md (default: ./mcp-vet-fixtures)')
|
|
128
|
-
.showHelpAfterError();
|
|
129
|
-
program.parse(process.argv);
|
|
130
|
-
const opts = program.opts();
|
|
131
|
-
const paths = program.args.length ? program.args : ['.'];
|
|
132
|
-
// --- Resolve configuration (CLI over config file over defaults) ---
|
|
133
|
-
let config = {};
|
|
134
|
-
try {
|
|
135
|
-
config = (0, config_1.loadConfig)(process.cwd(), opts.config);
|
|
101
|
+
// `mcp-vet probe [options] <url | command...>` — connect to a RUNNING server and
|
|
102
|
+
// vet its wire behavior (JSON Schema dialect, stateless-protocol readiness,
|
|
103
|
+
// server/discover, resource error code). `run` is an alias for `probe`.
|
|
104
|
+
// Async, so the scan pipeline only runs in the else-branch.
|
|
105
|
+
if (process.argv[2] === 'probe' || process.argv[2] === 'run') {
|
|
106
|
+
(0, probe_cli_1.runProbeCli)(process.argv.slice(3)).then((code) => process.exit(code), (err) => fail(err.message));
|
|
136
107
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
fail(err.message);
|
|
140
|
-
throw err;
|
|
108
|
+
else {
|
|
109
|
+
scanMain();
|
|
141
110
|
}
|
|
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
|
-
: config.pythonFallback != null
|
|
185
|
-
? config.pythonFallback
|
|
186
|
-
: opts.pyFallback;
|
|
187
|
-
const color = fromCli('color') ? opts.color : undefined;
|
|
188
|
-
// Ignore patterns: config + CLI + .mcpvetignore in cwd and each root dir
|
|
189
|
-
const ignorePatterns = new Set([...(config.ignore ?? []), ...opts.ignore]);
|
|
190
|
-
for (const line of readIgnoreFile(process.cwd()))
|
|
191
|
-
ignorePatterns.add(line);
|
|
192
|
-
for (const p of paths) {
|
|
111
|
+
function scanMain() {
|
|
112
|
+
const program = new commander_1.Command();
|
|
113
|
+
program
|
|
114
|
+
.name('mcp-vet')
|
|
115
|
+
.description('Scan MCP server source code for patterns that break under the 2026-07-28 MCP spec release candidate.')
|
|
116
|
+
.argument('[paths...]', 'files or directories to scan', ['.'])
|
|
117
|
+
.option('--github-annotations', 'emit GitHub Actions ::error/::warning annotations to stdout')
|
|
118
|
+
.option('--sarif [file]', 'write a SARIF 2.1.0 report (default file: mcp-vet.sarif)')
|
|
119
|
+
.option('--out-dir <dir>', 'directory for mcp-vet-report.md and mcp-vet-results.json', process.cwd())
|
|
120
|
+
.option('--no-files', 'do not write the markdown/json report files')
|
|
121
|
+
.option('--only <ids>', 'only run these pattern ids (comma/space separated)')
|
|
122
|
+
.option('--disable <ids>', 'skip these pattern ids (comma/space separated)')
|
|
123
|
+
.option('--fail-on <level>', `exit non-zero on: ${FAILON_VALUES.join(' | ')}`, 'breaking')
|
|
124
|
+
.option('--min-confidence <level>', `report only findings at/above: ${CONF_VALUES.join(' | ')}`, 'low')
|
|
125
|
+
.option('--ignore <glob>', 'ignore paths matching glob (repeatable)', (v, acc) => {
|
|
126
|
+
acc.push(v);
|
|
127
|
+
return acc;
|
|
128
|
+
}, [])
|
|
129
|
+
.option('--max-file-size <kb>', 'skip files larger than this many KB (0 = no limit)', '1536')
|
|
130
|
+
.option('--no-py-fallback', 'disable the regex fallback when no Python interpreter is found')
|
|
131
|
+
.option('--config <path>', 'path to a config file (.mcpvetrc.json)')
|
|
132
|
+
.option('--fix', 'auto-apply the safe mechanical fixes in place (currently: -32002 → -32602)')
|
|
133
|
+
.option('--dry-run', 'with --fix: print the rewrites that would be made, without changing files')
|
|
134
|
+
.option('--json', 'print findings as a JSON array to stdout (implies a quiet terminal report)')
|
|
135
|
+
.option('--color', 'force colored output')
|
|
136
|
+
.option('--no-color', 'disable colored output')
|
|
137
|
+
.option('--quiet', 'suppress the human-readable terminal report')
|
|
138
|
+
.version((0, constants_1.getVersion)(), '-v, --version')
|
|
139
|
+
.addHelpText('after', [
|
|
140
|
+
'',
|
|
141
|
+
'Commands:',
|
|
142
|
+
' fixtures [dir] write protocol-level conformance fixtures + CHECKLIST.md (default: ./mcp-vet-fixtures)',
|
|
143
|
+
' probe [options] <url|command> connect to a RUNNING server and vet its wire behavior (alias: run)',
|
|
144
|
+
' (JSON Schema 2020-12 dialect; with --spec-version 2026-07-28, also stateless',
|
|
145
|
+
' readiness, the required server/discover RPC, and the -32602 resource error code)',
|
|
146
|
+
].join('\n'))
|
|
147
|
+
.showHelpAfterError();
|
|
148
|
+
program.parse(process.argv);
|
|
149
|
+
const opts = program.opts();
|
|
150
|
+
const paths = program.args.length ? program.args : ['.'];
|
|
151
|
+
// --- Resolve configuration (CLI over config file over defaults) ---
|
|
152
|
+
let config = {};
|
|
193
153
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
154
|
+
config = (0, config_1.loadConfig)(process.cwd(), opts.config);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (err instanceof config_1.ConfigError)
|
|
158
|
+
fail(err.message);
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
// Validate enums
|
|
162
|
+
if (!FAILON_VALUES.includes(opts.failOn)) {
|
|
163
|
+
fail(`invalid --fail-on "${opts.failOn}". Valid: ${FAILON_VALUES.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
if (!CONF_VALUES.includes(opts.minConfidence)) {
|
|
166
|
+
fail(`invalid --min-confidence "${opts.minConfidence}". Valid: ${CONF_VALUES.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
// CLI value wins when explicitly set; otherwise fall back to the config file.
|
|
169
|
+
const fromCli = (key) => program.getOptionValueSource(key) === 'cli';
|
|
170
|
+
const failOn = fromCli('failOn')
|
|
171
|
+
? opts.failOn
|
|
172
|
+
: config.failOn ?? opts.failOn;
|
|
173
|
+
const minConfidence = fromCli('minConfidence')
|
|
174
|
+
? opts.minConfidence
|
|
175
|
+
: config.minConfidence ?? opts.minConfidence;
|
|
176
|
+
const normalizeIds = (ids) => ids
|
|
177
|
+
?.map((s) => String(s).toUpperCase())
|
|
178
|
+
.filter((s) => types_1.ALL_PATTERN_IDS.includes(s));
|
|
179
|
+
const cliOnly = parsePatternIds(opts.only);
|
|
180
|
+
const cliDisable = parsePatternIds(opts.disable);
|
|
181
|
+
const only = cliOnly ?? normalizeIds(config.only);
|
|
182
|
+
const disable = cliDisable ?? normalizeIds(config.disable);
|
|
183
|
+
let enabled = new Set(types_1.ALL_PATTERN_IDS);
|
|
184
|
+
if (only && only.length)
|
|
185
|
+
enabled = new Set(only.filter((id) => types_1.ALL_PATTERN_IDS.includes(id)));
|
|
186
|
+
// `disable` always applies on top — so a CLI --disable still narrows a config `only`.
|
|
187
|
+
if (disable && disable.length) {
|
|
188
|
+
for (const id of disable)
|
|
189
|
+
enabled.delete(id);
|
|
190
|
+
}
|
|
191
|
+
if (enabled.size === 0)
|
|
192
|
+
fail('no rules enabled after applying --only/--disable.');
|
|
193
|
+
const maxKbRaw = Number(opts.maxFileSize);
|
|
194
|
+
if (!Number.isFinite(maxKbRaw) || maxKbRaw < 0)
|
|
195
|
+
fail(`invalid --max-file-size "${opts.maxFileSize}".`);
|
|
196
|
+
const maxFileSizeKb = fromCli('maxFileSize')
|
|
197
|
+
? maxKbRaw
|
|
198
|
+
: config.maxFileSizeKb != null
|
|
199
|
+
? config.maxFileSizeKb
|
|
200
|
+
: maxKbRaw;
|
|
201
|
+
const pythonFallback = fromCli('pyFallback')
|
|
202
|
+
? opts.pyFallback
|
|
203
|
+
: config.pythonFallback != null
|
|
204
|
+
? config.pythonFallback
|
|
205
|
+
: opts.pyFallback;
|
|
206
|
+
const color = fromCli('color') ? opts.color : undefined;
|
|
207
|
+
// Ignore patterns: config + CLI + .mcpvetignore in cwd and each root dir
|
|
208
|
+
const ignorePatterns = new Set([...(config.ignore ?? []), ...opts.ignore]);
|
|
209
|
+
for (const line of readIgnoreFile(process.cwd()))
|
|
210
|
+
ignorePatterns.add(line);
|
|
211
|
+
for (const p of paths) {
|
|
212
|
+
try {
|
|
213
|
+
const abs = path.resolve(p);
|
|
214
|
+
if (fs.statSync(abs).isDirectory()) {
|
|
215
|
+
for (const line of readIgnoreFile(abs))
|
|
216
|
+
ignorePatterns.add(line);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
/* validated later in scan() */
|
|
198
221
|
}
|
|
199
222
|
}
|
|
200
|
-
|
|
201
|
-
|
|
223
|
+
const ignore = new ignore_1.IgnoreMatcher([...ignorePatterns]);
|
|
224
|
+
// --- Scan ---
|
|
225
|
+
let result;
|
|
226
|
+
try {
|
|
227
|
+
result = (0, scanner_1.scan)(paths, {
|
|
228
|
+
enabled,
|
|
229
|
+
ignore,
|
|
230
|
+
maxFileSizeKb,
|
|
231
|
+
pythonFallback,
|
|
232
|
+
minConfidence,
|
|
233
|
+
});
|
|
202
234
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const fr = (0, autofix_1.applyFixes)(result.findings, { dryRun: opts.dryRun });
|
|
225
|
-
if (opts.dryRun) {
|
|
226
|
-
if (fr.preview.length === 0) {
|
|
227
|
-
say('mcp-vet: --fix --dry-run — nothing to auto-fix.');
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err instanceof scanner_1.ScanError)
|
|
237
|
+
fail(err.message);
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
// --- Autofix (before reporting, so the report/exit reflect what remains) ---
|
|
241
|
+
if (opts.fix) {
|
|
242
|
+
const say = opts.json ? console.error : console.log; // keep stdout clean for --json
|
|
243
|
+
const fr = (0, autofix_1.applyFixes)(result.findings, { dryRun: opts.dryRun });
|
|
244
|
+
if (opts.dryRun) {
|
|
245
|
+
if (fr.preview.length === 0) {
|
|
246
|
+
say('mcp-vet: --fix --dry-run — nothing to auto-fix.');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
say(`mcp-vet: --fix --dry-run — ${fr.preview.length} rewrite(s) that would be applied (no files changed):`);
|
|
250
|
+
for (const p of fr.preview) {
|
|
251
|
+
say(` ${p.file}:${p.line}`);
|
|
252
|
+
say(` - ${p.before.trim()}`);
|
|
253
|
+
say(` + ${p.after.trim()}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
228
256
|
}
|
|
229
257
|
else {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
say(` - ${p.before.trim()}`);
|
|
234
|
-
say(` + ${p.after.trim()}`);
|
|
258
|
+
if (fr.fixedCount > 0) {
|
|
259
|
+
const fixed = new Set(fr.fixedFindings);
|
|
260
|
+
result.findings = result.findings.filter((f) => !fixed.has(f));
|
|
235
261
|
}
|
|
262
|
+
say(fr.fixedCount > 0
|
|
263
|
+
? `mcp-vet: fixed ${fr.fixedCount} occurrence(s) of -32002 → -32602 in ${fr.filesChanged.length} file(s).`
|
|
264
|
+
: 'mcp-vet: --fix found nothing to auto-fix.');
|
|
236
265
|
}
|
|
237
266
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
? `mcp-vet: fixed ${fr.fixedCount} occurrence(s) of -32002 → -32602 in ${fr.filesChanged.length} file(s).`
|
|
245
|
-
: 'mcp-vet: --fix found nothing to auto-fix.');
|
|
267
|
+
// --- Report ---
|
|
268
|
+
const quiet = opts.quiet || opts.json;
|
|
269
|
+
// Notices (Wrote ...) go to stderr in --json mode so stdout stays pure JSON.
|
|
270
|
+
const notify = (msg) => (opts.json ? console.error(msg) : console.log(msg));
|
|
271
|
+
if (opts.githubAnnotations) {
|
|
272
|
+
(0, reporters_1.printGithubAnnotations)(result.findings);
|
|
246
273
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const quiet = opts.quiet || opts.json;
|
|
250
|
-
// Notices (Wrote ...) go to stderr in --json mode so stdout stays pure JSON.
|
|
251
|
-
const notify = (msg) => (opts.json ? console.error(msg) : console.log(msg));
|
|
252
|
-
if (opts.githubAnnotations) {
|
|
253
|
-
(0, reporters_1.printGithubAnnotations)(result.findings);
|
|
254
|
-
}
|
|
255
|
-
if (!quiet) {
|
|
256
|
-
(0, reporters_1.reportTerminal)(result, { color });
|
|
257
|
-
}
|
|
258
|
-
if (opts.json) {
|
|
259
|
-
process.stdout.write((0, reporters_1.renderJson)(result) + '\n');
|
|
260
|
-
}
|
|
261
|
-
if (opts.sarif) {
|
|
262
|
-
const sarifPath = path.resolve(process.cwd(), typeof opts.sarif === 'string' ? opts.sarif : 'mcp-vet.sarif');
|
|
263
|
-
try {
|
|
264
|
-
(0, reporters_1.writeSarif)(result, sarifPath);
|
|
265
|
-
if (!quiet)
|
|
266
|
-
notify(`Wrote ${sarifPath}`);
|
|
274
|
+
if (!quiet) {
|
|
275
|
+
(0, reporters_1.reportTerminal)(result, { color });
|
|
267
276
|
}
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
if (opts.json) {
|
|
278
|
+
process.stdout.write((0, reporters_1.renderJson)(result) + '\n');
|
|
270
279
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
if (opts.sarif) {
|
|
281
|
+
const sarifPath = path.resolve(process.cwd(), typeof opts.sarif === 'string' ? opts.sarif : 'mcp-vet.sarif');
|
|
282
|
+
try {
|
|
283
|
+
(0, reporters_1.writeSarif)(result, sarifPath);
|
|
284
|
+
if (!quiet)
|
|
285
|
+
notify(`Wrote ${sarifPath}`);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
console.error(`mcp-vet: failed to write SARIF: ${err.message}`);
|
|
279
289
|
}
|
|
280
290
|
}
|
|
281
|
-
|
|
282
|
-
|
|
291
|
+
if (opts.files) {
|
|
292
|
+
try {
|
|
293
|
+
const md = (0, reporters_1.writeMarkdown)(result, opts.outDir);
|
|
294
|
+
const json = (0, reporters_1.writeJson)(result, opts.outDir);
|
|
295
|
+
if (!quiet) {
|
|
296
|
+
notify(`Wrote ${md}`);
|
|
297
|
+
notify(`Wrote ${json}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
console.error(`mcp-vet: failed to write report files: ${err.message}`);
|
|
302
|
+
}
|
|
283
303
|
}
|
|
304
|
+
// --- Exit code ---
|
|
305
|
+
const hasBreaking = result.findings.some((f) => f.severity === 'BREAKING' || f.severity === 'ERROR');
|
|
306
|
+
const hasAny = result.findings.length > 0;
|
|
307
|
+
let failing = false;
|
|
308
|
+
if (failOn === 'breaking')
|
|
309
|
+
failing = hasBreaking;
|
|
310
|
+
else if (failOn === 'any')
|
|
311
|
+
failing = hasAny;
|
|
312
|
+
else
|
|
313
|
+
failing = false; // 'none'
|
|
314
|
+
process.exit(failing ? 1 : 0);
|
|
284
315
|
}
|
|
285
|
-
// --- Exit code ---
|
|
286
|
-
const hasBreaking = result.findings.some((f) => f.severity === 'BREAKING');
|
|
287
|
-
const hasAny = result.findings.length > 0;
|
|
288
|
-
let failing = false;
|
|
289
|
-
if (failOn === 'breaking')
|
|
290
|
-
failing = hasBreaking;
|
|
291
|
-
else if (failOn === 'any')
|
|
292
|
-
failing = hasAny;
|
|
293
|
-
else
|
|
294
|
-
failing = false; // 'none'
|
|
295
|
-
process.exit(failing ? 1 : 0);
|