@booyaka/mcp-vet 0.4.0 → 0.5.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 +43 -0
- package/README.md +43 -4
- package/dist/autofix.d.ts +2 -2
- package/dist/autofix.js +2 -2
- package/dist/cli.js +196 -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 +33 -0
- package/dist/probe.js +472 -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 +23 -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 +6 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ 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.5.0]
|
|
8
|
+
|
|
9
|
+
The runtime-probe release — `mcp-vet probe` connects to a *running* MCP server
|
|
10
|
+
(stdio command or Streamable HTTP URL) and detects the two 2026-07-28 violation
|
|
11
|
+
categories that only exist on the wire, not in source.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`mcp-vet probe [options] <url | command...>`** — a runtime prober with two
|
|
16
|
+
new violation categories, reported in the same JSON + SARIF formats as the
|
|
17
|
+
static scan:
|
|
18
|
+
- **`json-schema-dialect` (WARN)** — calls `tools/list` and inspects every
|
|
19
|
+
tool's `inputSchema`/`outputSchema` (SEP-2106 lifts both to full JSON
|
|
20
|
+
Schema 2020-12). Flags an explicit draft-04/-06/-07 (or 2019-09) `$schema`
|
|
21
|
+
at high confidence, and — when `$schema` is absent — draft-only keyword
|
|
22
|
+
forms (`definitions`, `$ref: "#/definitions/…"`, boolean
|
|
23
|
+
`exclusiveMinimum`/`exclusiveMaximum`, array-form `items`, schema-form
|
|
24
|
+
`dependencies`) at medium confidence. The walker recurses only into schema
|
|
25
|
+
positions, so a *property* named `definitions` is never a false positive,
|
|
26
|
+
and an explicit 2020-12 `$schema` is trusted.
|
|
27
|
+
- **`requires-initialize-handshake` (ERROR)** — with
|
|
28
|
+
`--spec-version 2026-07-28`, makes a stateless first request (no
|
|
29
|
+
`initialize`; protocolVersion/clientInfo/capabilities travel in `_meta`
|
|
30
|
+
per the RC) and flags a server that rejects it or hangs. Cross-checked:
|
|
31
|
+
only emitted when the classic 2025-11-25 handshake path *does* work, so a
|
|
32
|
+
dead server is an operational error (exit 2), never a false violation.
|
|
33
|
+
- **`--spec-version <2025-11-25|2026-07-28>`** (default `2025-11-25`) selects
|
|
34
|
+
the revision to vet against; `--timeout <ms>` bounds each request and doubles
|
|
35
|
+
as the hang-detection window; `--json` / `--sarif [file]` / `--fail-on` /
|
|
36
|
+
`--quiet` / `--color` work as in the scan.
|
|
37
|
+
- **Runtime rules in SARIF** — probe rules join the driver metadata when they
|
|
38
|
+
fire (`ERROR` → `error`, `WARN` → `warning`); the static-scan SARIF keeps its
|
|
39
|
+
stable 9-rule shape.
|
|
40
|
+
- **`test/probe-fixtures/`** — minimal real MCP servers used by 18 new tests:
|
|
41
|
+
`server-draft07.mjs` (explicit + inferable draft-07 tools, a modern 2020-12
|
|
42
|
+
tool, and a property literally named `definitions`), `server-requires-init.mjs`
|
|
43
|
+
(rejects pre-initialize requests with `-32002`), `server-stateless.mjs`
|
|
44
|
+
(2026-07-28-native, requires `_meta`, no initialize), and `server-http.mjs`
|
|
45
|
+
(Streamable HTTP, sessionful *and* stateless modes).
|
|
46
|
+
- Verified against the official `@modelcontextprotocol/server-everything@2026.7.4`:
|
|
47
|
+
it answers stateless requests, but all of its tool schemas still declare
|
|
48
|
+
draft-07 — `probe` reports 14 true `json-schema-dialect` findings.
|
|
49
|
+
|
|
7
50
|
## [0.4.0]
|
|
8
51
|
|
|
9
52
|
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,44 @@ 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 two 2026-07-28 violations that only exist at runtime:
|
|
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`, capabilities/clientInfo/protocolVersion in `_meta` per the RC — and flags a server that rejects it or hangs waiting for the removed handshake |
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# vet the schemas of a stdio server (spawns the command; a lone .js file runs with Node)
|
|
227
|
+
npx @booyaka/mcp-vet probe node ./dist/server.js
|
|
228
|
+
|
|
229
|
+
# full 2026-07-28 readiness: stateless first contact + schema dialects
|
|
230
|
+
npx @booyaka/mcp-vet probe --spec-version 2026-07-28 http://localhost:3000/mcp
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```text
|
|
234
|
+
mcp-vet probe — node ./dist/server.js · spec 2026-07-28 · stdio · 12 tool(s) listed
|
|
235
|
+
stateless probe: stateless tools/list was rejected: -32002 Server not initialized
|
|
236
|
+
fallback probe: initialize handshake + tools/list succeeded
|
|
237
|
+
|
|
238
|
+
ERROR requires-initialize-handshake [high]
|
|
239
|
+
The server rejected (or hung on) a stateless 2026-07-28-style first request ...
|
|
240
|
+
WARN json-schema-dialect [high]
|
|
241
|
+
tool "echo" inputSchema: $schema = http://json-schema.org/draft-07/schema# (draft-07)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
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 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.
|
|
245
|
+
|
|
246
|
+
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`.
|
|
247
|
+
|
|
248
|
+
Try it against the official reference server — the July 2026 `@modelcontextprotocol/server-everything` answers stateless requests, but its tool schemas still declare draft-07, and `probe` catches all of them:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npx @booyaka/mcp-vet probe --spec-version 2026-07-28 npx -y mcp-server-everything stdio
|
|
252
|
+
```
|
|
253
|
+
|
|
216
254
|
## Usage
|
|
217
255
|
|
|
218
256
|
```bash
|
|
@@ -221,6 +259,7 @@ npx @booyaka/mcp-vet . --fix # scan, and auto-apply the mechanical -32
|
|
|
221
259
|
npx @booyaka/mcp-vet ./src ./packages # multiple roots
|
|
222
260
|
npx @booyaka/mcp-vet server.py # a single file
|
|
223
261
|
npx @booyaka/mcp-vet fixtures ./dir # write runtime conformance fixtures + checklist (default: ./mcp-vet-fixtures)
|
|
262
|
+
npx @booyaka/mcp-vet probe <url|cmd> # vet a RUNNING server's wire behavior (see section above)
|
|
224
263
|
```
|
|
225
264
|
|
|
226
265
|
Globs `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and `**/*.py`, skipping `node_modules`, `.git`, `__pycache__`, `dist`, and `build`.
|
|
@@ -426,10 +465,10 @@ Also exported: `renderJson` / `renderMarkdown` / `renderSarif`, `RULES`, and the
|
|
|
426
465
|
```bash
|
|
427
466
|
npm install # installs deps and builds (via prepare)
|
|
428
467
|
npm run build # tsc -> dist/ + copies the Python script
|
|
429
|
-
npm test # builds, then runs the Node.js built-in test runner (
|
|
468
|
+
npm test # builds, then runs the Node.js built-in test runner (55 tests)
|
|
430
469
|
```
|
|
431
470
|
|
|
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).
|
|
471
|
+
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, and one fully stateless 2026-07-28-native.
|
|
433
472
|
|
|
434
473
|
## License
|
|
435
474
|
|
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,216 @@ if (process.argv[2] === 'fixtures') {
|
|
|
97
98
|
fail(`could not write fixtures: ${err.message}`);
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.option('--github-annotations', 'emit GitHub Actions ::error/::warning annotations to stdout')
|
|
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
|
+
// Async, so the scan pipeline only runs in the else-branch.
|
|
104
|
+
if (process.argv[2] === 'probe') {
|
|
105
|
+
(0, probe_cli_1.runProbeCli)(process.argv.slice(3)).then((code) => process.exit(code), (err) => fail(err.message));
|
|
136
106
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
fail(err.message);
|
|
140
|
-
throw err;
|
|
107
|
+
else {
|
|
108
|
+
scanMain();
|
|
141
109
|
}
|
|
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
|
-
? opts.pyFallback
|
|
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) {
|
|
110
|
+
function scanMain() {
|
|
111
|
+
const program = new commander_1.Command();
|
|
112
|
+
program
|
|
113
|
+
.name('mcp-vet')
|
|
114
|
+
.description('Scan MCP server source code for patterns that break under the 2026-07-28 MCP spec release candidate.')
|
|
115
|
+
.argument('[paths...]', 'files or directories to scan', ['.'])
|
|
116
|
+
.option('--github-annotations', 'emit GitHub Actions ::error/::warning annotations to stdout')
|
|
117
|
+
.option('--sarif [file]', 'write a SARIF 2.1.0 report (default file: mcp-vet.sarif)')
|
|
118
|
+
.option('--out-dir <dir>', 'directory for mcp-vet-report.md and mcp-vet-results.json', process.cwd())
|
|
119
|
+
.option('--no-files', 'do not write the markdown/json report files')
|
|
120
|
+
.option('--only <ids>', 'only run these pattern ids (comma/space separated)')
|
|
121
|
+
.option('--disable <ids>', 'skip these pattern ids (comma/space separated)')
|
|
122
|
+
.option('--fail-on <level>', `exit non-zero on: ${FAILON_VALUES.join(' | ')}`, 'breaking')
|
|
123
|
+
.option('--min-confidence <level>', `report only findings at/above: ${CONF_VALUES.join(' | ')}`, 'low')
|
|
124
|
+
.option('--ignore <glob>', 'ignore paths matching glob (repeatable)', (v, acc) => {
|
|
125
|
+
acc.push(v);
|
|
126
|
+
return acc;
|
|
127
|
+
}, [])
|
|
128
|
+
.option('--max-file-size <kb>', 'skip files larger than this many KB (0 = no limit)', '1536')
|
|
129
|
+
.option('--no-py-fallback', 'disable the regex fallback when no Python interpreter is found')
|
|
130
|
+
.option('--config <path>', 'path to a config file (.mcpvetrc.json)')
|
|
131
|
+
.option('--fix', 'auto-apply the safe mechanical fixes in place (currently: -32002 → -32602)')
|
|
132
|
+
.option('--dry-run', 'with --fix: print the rewrites that would be made, without changing files')
|
|
133
|
+
.option('--json', 'print findings as a JSON array to stdout (implies a quiet terminal report)')
|
|
134
|
+
.option('--color', 'force colored output')
|
|
135
|
+
.option('--no-color', 'disable colored output')
|
|
136
|
+
.option('--quiet', 'suppress the human-readable terminal report')
|
|
137
|
+
.version((0, constants_1.getVersion)(), '-v, --version')
|
|
138
|
+
.addHelpText('after', [
|
|
139
|
+
'',
|
|
140
|
+
'Commands:',
|
|
141
|
+
' fixtures [dir] write protocol-level conformance fixtures + CHECKLIST.md (default: ./mcp-vet-fixtures)',
|
|
142
|
+
' probe [options] <url|command> connect to a RUNNING server and vet its wire behavior',
|
|
143
|
+
' (JSON Schema 2020-12 dialect; with --spec-version 2026-07-28, stateless readiness)',
|
|
144
|
+
].join('\n'))
|
|
145
|
+
.showHelpAfterError();
|
|
146
|
+
program.parse(process.argv);
|
|
147
|
+
const opts = program.opts();
|
|
148
|
+
const paths = program.args.length ? program.args : ['.'];
|
|
149
|
+
// --- Resolve configuration (CLI over config file over defaults) ---
|
|
150
|
+
let config = {};
|
|
193
151
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
152
|
+
config = (0, config_1.loadConfig)(process.cwd(), opts.config);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (err instanceof config_1.ConfigError)
|
|
156
|
+
fail(err.message);
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
// Validate enums
|
|
160
|
+
if (!FAILON_VALUES.includes(opts.failOn)) {
|
|
161
|
+
fail(`invalid --fail-on "${opts.failOn}". Valid: ${FAILON_VALUES.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (!CONF_VALUES.includes(opts.minConfidence)) {
|
|
164
|
+
fail(`invalid --min-confidence "${opts.minConfidence}". Valid: ${CONF_VALUES.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
// CLI value wins when explicitly set; otherwise fall back to the config file.
|
|
167
|
+
const fromCli = (key) => program.getOptionValueSource(key) === 'cli';
|
|
168
|
+
const failOn = fromCli('failOn')
|
|
169
|
+
? opts.failOn
|
|
170
|
+
: config.failOn ?? opts.failOn;
|
|
171
|
+
const minConfidence = fromCli('minConfidence')
|
|
172
|
+
? opts.minConfidence
|
|
173
|
+
: config.minConfidence ?? opts.minConfidence;
|
|
174
|
+
const normalizeIds = (ids) => ids
|
|
175
|
+
?.map((s) => String(s).toUpperCase())
|
|
176
|
+
.filter((s) => types_1.ALL_PATTERN_IDS.includes(s));
|
|
177
|
+
const cliOnly = parsePatternIds(opts.only);
|
|
178
|
+
const cliDisable = parsePatternIds(opts.disable);
|
|
179
|
+
const only = cliOnly ?? normalizeIds(config.only);
|
|
180
|
+
const disable = cliDisable ?? normalizeIds(config.disable);
|
|
181
|
+
let enabled = new Set(types_1.ALL_PATTERN_IDS);
|
|
182
|
+
if (only && only.length)
|
|
183
|
+
enabled = new Set(only.filter((id) => types_1.ALL_PATTERN_IDS.includes(id)));
|
|
184
|
+
// `disable` always applies on top — so a CLI --disable still narrows a config `only`.
|
|
185
|
+
if (disable && disable.length) {
|
|
186
|
+
for (const id of disable)
|
|
187
|
+
enabled.delete(id);
|
|
188
|
+
}
|
|
189
|
+
if (enabled.size === 0)
|
|
190
|
+
fail('no rules enabled after applying --only/--disable.');
|
|
191
|
+
const maxKbRaw = Number(opts.maxFileSize);
|
|
192
|
+
if (!Number.isFinite(maxKbRaw) || maxKbRaw < 0)
|
|
193
|
+
fail(`invalid --max-file-size "${opts.maxFileSize}".`);
|
|
194
|
+
const maxFileSizeKb = fromCli('maxFileSize')
|
|
195
|
+
? maxKbRaw
|
|
196
|
+
: config.maxFileSizeKb != null
|
|
197
|
+
? config.maxFileSizeKb
|
|
198
|
+
: maxKbRaw;
|
|
199
|
+
const pythonFallback = fromCli('pyFallback')
|
|
200
|
+
? opts.pyFallback
|
|
201
|
+
: config.pythonFallback != null
|
|
202
|
+
? config.pythonFallback
|
|
203
|
+
: opts.pyFallback;
|
|
204
|
+
const color = fromCli('color') ? opts.color : undefined;
|
|
205
|
+
// Ignore patterns: config + CLI + .mcpvetignore in cwd and each root dir
|
|
206
|
+
const ignorePatterns = new Set([...(config.ignore ?? []), ...opts.ignore]);
|
|
207
|
+
for (const line of readIgnoreFile(process.cwd()))
|
|
208
|
+
ignorePatterns.add(line);
|
|
209
|
+
for (const p of paths) {
|
|
210
|
+
try {
|
|
211
|
+
const abs = path.resolve(p);
|
|
212
|
+
if (fs.statSync(abs).isDirectory()) {
|
|
213
|
+
for (const line of readIgnoreFile(abs))
|
|
214
|
+
ignorePatterns.add(line);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* validated later in scan() */
|
|
198
219
|
}
|
|
199
220
|
}
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
const ignore = new ignore_1.IgnoreMatcher([...ignorePatterns]);
|
|
222
|
+
// --- Scan ---
|
|
223
|
+
let result;
|
|
224
|
+
try {
|
|
225
|
+
result = (0, scanner_1.scan)(paths, {
|
|
226
|
+
enabled,
|
|
227
|
+
ignore,
|
|
228
|
+
maxFileSizeKb,
|
|
229
|
+
pythonFallback,
|
|
230
|
+
minConfidence,
|
|
231
|
+
});
|
|
202
232
|
}
|
|
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.');
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err instanceof scanner_1.ScanError)
|
|
235
|
+
fail(err.message);
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
// --- Autofix (before reporting, so the report/exit reflect what remains) ---
|
|
239
|
+
if (opts.fix) {
|
|
240
|
+
const say = opts.json ? console.error : console.log; // keep stdout clean for --json
|
|
241
|
+
const fr = (0, autofix_1.applyFixes)(result.findings, { dryRun: opts.dryRun });
|
|
242
|
+
if (opts.dryRun) {
|
|
243
|
+
if (fr.preview.length === 0) {
|
|
244
|
+
say('mcp-vet: --fix --dry-run — nothing to auto-fix.');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
say(`mcp-vet: --fix --dry-run — ${fr.preview.length} rewrite(s) that would be applied (no files changed):`);
|
|
248
|
+
for (const p of fr.preview) {
|
|
249
|
+
say(` ${p.file}:${p.line}`);
|
|
250
|
+
say(` - ${p.before.trim()}`);
|
|
251
|
+
say(` + ${p.after.trim()}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
228
254
|
}
|
|
229
255
|
else {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
say(` - ${p.before.trim()}`);
|
|
234
|
-
say(` + ${p.after.trim()}`);
|
|
256
|
+
if (fr.fixedCount > 0) {
|
|
257
|
+
const fixed = new Set(fr.fixedFindings);
|
|
258
|
+
result.findings = result.findings.filter((f) => !fixed.has(f));
|
|
235
259
|
}
|
|
260
|
+
say(fr.fixedCount > 0
|
|
261
|
+
? `mcp-vet: fixed ${fr.fixedCount} occurrence(s) of -32002 → -32602 in ${fr.filesChanged.length} file(s).`
|
|
262
|
+
: 'mcp-vet: --fix found nothing to auto-fix.');
|
|
236
263
|
}
|
|
237
264
|
}
|
|
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.');
|
|
265
|
+
// --- Report ---
|
|
266
|
+
const quiet = opts.quiet || opts.json;
|
|
267
|
+
// Notices (Wrote ...) go to stderr in --json mode so stdout stays pure JSON.
|
|
268
|
+
const notify = (msg) => (opts.json ? console.error(msg) : console.log(msg));
|
|
269
|
+
if (opts.githubAnnotations) {
|
|
270
|
+
(0, reporters_1.printGithubAnnotations)(result.findings);
|
|
246
271
|
}
|
|
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}`);
|
|
272
|
+
if (!quiet) {
|
|
273
|
+
(0, reporters_1.reportTerminal)(result, { color });
|
|
267
274
|
}
|
|
268
|
-
|
|
269
|
-
|
|
275
|
+
if (opts.json) {
|
|
276
|
+
process.stdout.write((0, reporters_1.renderJson)(result) + '\n');
|
|
270
277
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
278
|
+
if (opts.sarif) {
|
|
279
|
+
const sarifPath = path.resolve(process.cwd(), typeof opts.sarif === 'string' ? opts.sarif : 'mcp-vet.sarif');
|
|
280
|
+
try {
|
|
281
|
+
(0, reporters_1.writeSarif)(result, sarifPath);
|
|
282
|
+
if (!quiet)
|
|
283
|
+
notify(`Wrote ${sarifPath}`);
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
console.error(`mcp-vet: failed to write SARIF: ${err.message}`);
|
|
279
287
|
}
|
|
280
288
|
}
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
if (opts.files) {
|
|
290
|
+
try {
|
|
291
|
+
const md = (0, reporters_1.writeMarkdown)(result, opts.outDir);
|
|
292
|
+
const json = (0, reporters_1.writeJson)(result, opts.outDir);
|
|
293
|
+
if (!quiet) {
|
|
294
|
+
notify(`Wrote ${md}`);
|
|
295
|
+
notify(`Wrote ${json}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
console.error(`mcp-vet: failed to write report files: ${err.message}`);
|
|
300
|
+
}
|
|
283
301
|
}
|
|
302
|
+
// --- Exit code ---
|
|
303
|
+
const hasBreaking = result.findings.some((f) => f.severity === 'BREAKING' || f.severity === 'ERROR');
|
|
304
|
+
const hasAny = result.findings.length > 0;
|
|
305
|
+
let failing = false;
|
|
306
|
+
if (failOn === 'breaking')
|
|
307
|
+
failing = hasBreaking;
|
|
308
|
+
else if (failOn === 'any')
|
|
309
|
+
failing = hasAny;
|
|
310
|
+
else
|
|
311
|
+
failing = false; // 'none'
|
|
312
|
+
process.exit(failing ? 1 : 0);
|
|
284
313
|
}
|
|
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);
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare const SPEC_DATE = "July 28, 2026";
|
|
2
2
|
export declare const SPEC_URL = "https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/";
|
|
3
3
|
export declare const CHANGELOG_URL = "https://tokenmix.ai/blog/mcp-updates-changelog-every-protocol-change-2026";
|
|
4
|
+
export declare const SEP_2106_URL = "https://modelcontextprotocol.io/seps/2106-json-schema-2020-12";
|
|
5
|
+
export declare const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
4
6
|
/**
|
|
5
7
|
* 2026-07-28 changes that are real but NOT reliably detectable by static token
|
|
6
8
|
* analysis — surfaced to the user so the tool is honest about its scope rather
|
package/dist/constants.js
CHANGED
|
@@ -33,13 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.MANUAL_REVIEW = exports.CHANGELOG_URL = exports.SPEC_URL = exports.SPEC_DATE = void 0;
|
|
36
|
+
exports.MANUAL_REVIEW = exports.JSON_SCHEMA_2020_12 = exports.SEP_2106_URL = exports.CHANGELOG_URL = exports.SPEC_URL = exports.SPEC_DATE = void 0;
|
|
37
37
|
exports.getVersion = getVersion;
|
|
38
38
|
const fs = __importStar(require("node:fs"));
|
|
39
39
|
const path = __importStar(require("node:path"));
|
|
40
40
|
exports.SPEC_DATE = 'July 28, 2026';
|
|
41
41
|
exports.SPEC_URL = 'https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/';
|
|
42
42
|
exports.CHANGELOG_URL = 'https://tokenmix.ai/blog/mcp-updates-changelog-every-protocol-change-2026';
|
|
43
|
+
exports.SEP_2106_URL = 'https://modelcontextprotocol.io/seps/2106-json-schema-2020-12';
|
|
44
|
+
exports.JSON_SCHEMA_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
|
|
43
45
|
/**
|
|
44
46
|
* 2026-07-28 changes that are real but NOT reliably detectable by static token
|
|
45
47
|
* analysis — surfaced to the user so the tool is honest about its scope rather
|
|
@@ -49,7 +51,7 @@ exports.MANUAL_REVIEW = [
|
|
|
49
51
|
'the long-lived server→client SSE push channel is removed (a server may only send requests while handling one)',
|
|
50
52
|
'Streamable HTTP now requires Mcp-Method and Mcp-Name headers that mirror the JSON-RPC body',
|
|
51
53
|
'auth hardening: validate the RFC 9207 `iss` param, send OIDC `application_type`, bind tokens to the issuer',
|
|
52
|
-
'tool inputSchema/outputSchema may now be full JSON Schema 2020-12 (do not auto-dereference external $ref)',
|
|
54
|
+
'tool inputSchema/outputSchema may now be full JSON Schema 2020-12 (do not auto-dereference external $ref) — `mcp-vet probe <server>` checks the dialect of a running server',
|
|
53
55
|
];
|
|
54
56
|
/** Resolve the package version from package.json, tolerating layout differences. */
|
|
55
57
|
function getVersion() {
|
package/dist/index.d.ts
CHANGED
|
@@ -22,10 +22,15 @@ export type { ScanOptions, ScanResult, PythonMode } from './scanner';
|
|
|
22
22
|
export { applyFixes, isFixable } from './autofix';
|
|
23
23
|
export type { FixResult } from './autofix';
|
|
24
24
|
export { renderJson, renderMarkdown, renderSarif, toPublicFinding } from './reporters';
|
|
25
|
-
export { RULES } from './rules';
|
|
25
|
+
export { RULES, RUNTIME_RULES } from './rules';
|
|
26
|
+
export type { RuntimeRuleMeta } from './rules';
|
|
26
27
|
export { CONFORMANCE_FIXTURES, emitConformanceFixtures } from './conformance';
|
|
27
28
|
export type { ConformanceFixture, ConformanceStep, EmitResult } from './conformance';
|
|
28
29
|
export { IgnoreMatcher } from './ignore';
|
|
29
|
-
export { SPEC_URL, SPEC_DATE, CHANGELOG_URL, MANUAL_REVIEW, getVersion } from './constants';
|
|
30
|
-
export {
|
|
31
|
-
export type {
|
|
30
|
+
export { SPEC_URL, SPEC_DATE, CHANGELOG_URL, SEP_2106_URL, JSON_SCHEMA_2020_12, MANUAL_REVIEW, getVersion, } from './constants';
|
|
31
|
+
export { probeServer, ProbeError, targetLabel } from './probe';
|
|
32
|
+
export type { ProbeTarget, ProbeOptions, ProbeResult } from './probe';
|
|
33
|
+
export { analyzeSchemaDialect } from './schema-dialect';
|
|
34
|
+
export type { DialectIssue } from './schema-dialect';
|
|
35
|
+
export { ALL_PATTERN_IDS, ALL_RUNTIME_RULE_IDS, SPEC_VERSIONS } from './types';
|
|
36
|
+
export type { Finding, PatternId, RuntimeRuleId, ViolationId, SpecVersion, Severity, Confidence, Token, } from './types';
|