@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 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, no network calls — it parses your code locally (ts-morph for TS/JS, a bundled Python `ast` script for `.py`) and exits non-zero if it finds anything **BREAKING**, so you can drop it straight into CI.
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 (30 tests)
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, PatternId } from './types';
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: PatternId): boolean;
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 — a
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 ? '' : '') + lines.join('\n');
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
- const program = new commander_1.Command();
101
- program
102
- .name('mcp-vet')
103
- .description('Scan MCP server source code for patterns that break under the 2026-07-28 MCP spec release candidate.')
104
- .argument('[paths...]', 'files or directories to scan', ['.'])
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
- catch (err) {
138
- if (err instanceof config_1.ConfigError)
139
- fail(err.message);
140
- throw err;
107
+ else {
108
+ scanMain();
141
109
  }
142
- // Validate enums
143
- if (!FAILON_VALUES.includes(opts.failOn)) {
144
- fail(`invalid --fail-on "${opts.failOn}". Valid: ${FAILON_VALUES.join(', ')}`);
145
- }
146
- if (!CONF_VALUES.includes(opts.minConfidence)) {
147
- fail(`invalid --min-confidence "${opts.minConfidence}". Valid: ${CONF_VALUES.join(', ')}`);
148
- }
149
- // CLI value wins when explicitly set; otherwise fall back to the config file.
150
- const fromCli = (key) => program.getOptionValueSource(key) === 'cli';
151
- const failOn = fromCli('failOn')
152
- ? opts.failOn
153
- : config.failOn ?? opts.failOn;
154
- const minConfidence = fromCli('minConfidence')
155
- ? opts.minConfidence
156
- : config.minConfidence ?? opts.minConfidence;
157
- const normalizeIds = (ids) => ids
158
- ?.map((s) => String(s).toUpperCase())
159
- .filter((s) => types_1.ALL_PATTERN_IDS.includes(s));
160
- const cliOnly = parsePatternIds(opts.only);
161
- const cliDisable = parsePatternIds(opts.disable);
162
- const only = cliOnly ?? normalizeIds(config.only);
163
- const disable = cliDisable ?? normalizeIds(config.disable);
164
- let enabled = new Set(types_1.ALL_PATTERN_IDS);
165
- if (only && only.length)
166
- enabled = new Set(only.filter((id) => types_1.ALL_PATTERN_IDS.includes(id)));
167
- // `disable` always applies on top — so a CLI --disable still narrows a config `only`.
168
- if (disable && disable.length) {
169
- for (const id of disable)
170
- enabled.delete(id);
171
- }
172
- if (enabled.size === 0)
173
- fail('no rules enabled after applying --only/--disable.');
174
- const maxKbRaw = Number(opts.maxFileSize);
175
- if (!Number.isFinite(maxKbRaw) || maxKbRaw < 0)
176
- fail(`invalid --max-file-size "${opts.maxFileSize}".`);
177
- const maxFileSizeKb = fromCli('maxFileSize')
178
- ? maxKbRaw
179
- : config.maxFileSizeKb != null
180
- ? config.maxFileSizeKb
181
- : maxKbRaw;
182
- const pythonFallback = fromCli('pyFallback')
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
- const abs = path.resolve(p);
195
- if (fs.statSync(abs).isDirectory()) {
196
- for (const line of readIgnoreFile(abs))
197
- ignorePatterns.add(line);
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
- catch {
201
- /* validated later in scan() */
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
- const ignore = new ignore_1.IgnoreMatcher([...ignorePatterns]);
205
- // --- Scan ---
206
- let result;
207
- try {
208
- result = (0, scanner_1.scan)(paths, {
209
- enabled,
210
- ignore,
211
- maxFileSizeKb,
212
- pythonFallback,
213
- minConfidence,
214
- });
215
- }
216
- catch (err) {
217
- if (err instanceof scanner_1.ScanError)
218
- fail(err.message);
219
- throw err;
220
- }
221
- // --- Autofix (before reporting, so the report/exit reflect what remains) ---
222
- if (opts.fix) {
223
- const say = opts.json ? console.error : console.log; // keep stdout clean for --json
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
- say(`mcp-vet: --fix --dry-run — ${fr.preview.length} rewrite(s) that would be applied (no files changed):`);
231
- for (const p of fr.preview) {
232
- say(` ${p.file}:${p.line}`);
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
- else {
239
- if (fr.fixedCount > 0) {
240
- const fixed = new Set(fr.fixedFindings);
241
- result.findings = result.findings.filter((f) => !fixed.has(f));
242
- }
243
- say(fr.fixedCount > 0
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
- // --- Report ---
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
- catch (err) {
269
- console.error(`mcp-vet: failed to write SARIF: ${err.message}`);
275
+ if (opts.json) {
276
+ process.stdout.write((0, reporters_1.renderJson)(result) + '\n');
270
277
  }
271
- }
272
- if (opts.files) {
273
- try {
274
- const md = (0, reporters_1.writeMarkdown)(result, opts.outDir);
275
- const json = (0, reporters_1.writeJson)(result, opts.outDir);
276
- if (!quiet) {
277
- notify(`Wrote ${md}`);
278
- notify(`Wrote ${json}`);
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
- catch (err) {
282
- console.error(`mcp-vet: failed to write report files: ${err.message}`);
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);
@@ -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 { ALL_PATTERN_IDS } from './types';
31
- export type { Finding, PatternId, Severity, Confidence, Token } from './types';
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';