@booyaka/mcp-vet 0.2.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 ADDED
@@ -0,0 +1,54 @@
1
+ # Changelog
2
+
3
+ All notable changes to `mcp-vet` are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.2.0]
8
+
9
+ The robustness and precision release. Everything below is covered by the test suite.
10
+
11
+ ### Added
12
+
13
+ - **Confidence scoring** (`high` / `medium` / `low`) on every finding, plus
14
+ `--min-confidence` to tune signal-to-noise. Capability findings are `high` only
15
+ when the key is *structurally* inside a `capabilities` object (AST-verified),
16
+ `medium` when merely near a `capabilities` mention. `initialize` is `high` only
17
+ in a registration / `switch` / `req.method ===` context, `low` as a bare string.
18
+ - **SARIF 2.1.0 output** (`--sarif [file]`) for GitHub Advanced Security code
19
+ scanning, with per-rule metadata, levels, and line/column regions.
20
+ - **Inline suppression**: `mcp-vet-disable-line`, `-next-line`, and `-file`
21
+ comments, with optional pattern-id lists and a suppressed-count summary.
22
+ - **Rule selection & gating**: `--only`, `--disable`, and `--fail-on
23
+ breaking|any|none`.
24
+ - **Config file** support (`.mcpvetrc.json` / `mcp-vet.config.json`) and a
25
+ `.mcpvetignore` file; CLI flags override config.
26
+ - **Ignore globs** (`--ignore`, repeatable) and `--max-file-size`.
27
+ - **Richer findings**: column / end-column, `docUrl`, and the source analyzer
28
+ (`ts-morph` / `python-ast` / `regex`) on every finding.
29
+ - Distinct **exit code `2`** for operational errors (bad path, bad config,
30
+ invalid flag) — separate from `1` (findings tripped the gate).
31
+ - `--color` / `--no-color` and `--quiet`.
32
+
33
+ ### Changed
34
+
35
+ - Python scanning runs in a **chunked subprocess** so one unparseable file can't
36
+ sink the batch, with a **regex fallback** when no interpreter is present
37
+ (`--no-py-fallback` to require the AST path).
38
+ - UTF-8 BOM and CRLF files are scanned with correct line numbers.
39
+ - Findings are de-duplicated per `(line, column, rule)`.
40
+
41
+ ## [0.1.0]
42
+
43
+ Initial release.
44
+
45
+ - Detects the 7 patterns affected by the MCP `2026-07-28` specification: the
46
+ `Mcp-Session-Id` header, the `initialize` / `notifications/initialized`
47
+ handshake, the `-32002` error code, the legacy `tasks/get|update|cancel`
48
+ methods, and the deprecated `roots` / `sampling` / `logging` capabilities.
49
+ - AST analysis for TypeScript / JavaScript (`ts-morph`) and Python (`ast`), a
50
+ shared rule engine, and four report formats (terminal, Markdown, JSON, GitHub
51
+ Actions annotations).
52
+
53
+ [0.2.0]: https://github.com/Booyaka101/mcp-vet/releases/tag/v0.2.0
54
+ [0.1.0]: https://github.com/Booyaka101/mcp-vet/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mcp-vet contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # mcp-vet
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@booyaka/mcp-vet.svg)](https://www.npmjs.com/package/@booyaka/mcp-vet)
4
+ [![CI](https://github.com/Booyaka101/mcp-vet/actions/workflows/ci.yml/badge.svg)](https://github.com/Booyaka101/mcp-vet/actions/workflows/ci.yml)
5
+ [![node](https://img.shields.io/node/v/@booyaka/mcp-vet.svg)](https://nodejs.org)
6
+ [![license: MIT](https://img.shields.io/npm/l/@booyaka/mcp-vet.svg)](./LICENSE)
7
+
8
+ **On July 28, 2026 the Model Context Protocol ships its `2026-07-28` specification as final** — and it removes several things that today's MCP servers rely on. `mcp-vet` is a zero-config CLI that scans your MCP server source (TypeScript, JavaScript, and Python) for the exact patterns that will break client interop on that date, and tells you what to change.
9
+
10
+ - Official release candidate: <https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/>
11
+ - Protocol changelog: <https://tokenmix.ai/blog/mcp-updates-changelog-every-protocol-change-2026>
12
+
13
+ ```bash
14
+ npx @booyaka/mcp-vet .
15
+ ```
16
+
17
+ 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.
18
+
19
+ ---
20
+
21
+ ## Real-world example
22
+
23
+ Pointed at the [official MCP TypeScript SDK's own example servers](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples), `mcp-vet` finds the patterns that the `2026-07-28` spec breaks:
24
+
25
+ ```text
26
+ legacy-routing.ts:36:29 BREAKING MCP_SESSION_ID [high]
27
+ const sid = req.headers['mcp-session-id'] as string | undefined;
28
+ legacy-routing.ts:70:26 BREAKING MCP_SESSION_ID [high]
29
+ exposedHeaders: ['Mcp-Session-Id', 'WWW-Authenticate', ...]
30
+ sse-polling.ts:34:29 DEPRECATED LOGGING_CAP [high]
31
+ capabilities: { logging: {} }
32
+ sse-polling.ts:102:29 BREAKING MCP_SESSION_ID [high]
33
+ const sid = req.headers['mcp-session-id'] as string | undefined;
34
+
35
+ 4 finding(s): 3 BREAKING, 1 DEPRECATED
36
+ ```
37
+
38
+ And it stays quiet where it should — the `initialize` mentioned in a *comment* in `dual-era.ts`, and the `sampling/createMessage` **method** in `sampling.ts`, are not flagged, because only the `sampling` **capability declaration** is deprecated, not the method. That precision (structural AST checks, not text matching) is what keeps the noise down on a real codebase.
39
+
40
+ ---
41
+
42
+ ## What it detects
43
+
44
+ ### 🔴 BREAKING (fails the build — exit code 1)
45
+
46
+ | ID | Pattern |
47
+ | --- | --- |
48
+ | `MCP_SESSION_ID` | `Mcp-Session-Id` header / `mcpSessionId` variable |
49
+ | `INITIALIZE_HANDLER` | `initialize` / `notifications/initialized` handler registration |
50
+ | `ERROR_CODE_32002` | the numeric error code `-32002` |
51
+ | `TASKS_LEGACY` | `tasks/get` · `tasks/update` · `tasks/cancel` legacy method strings |
52
+
53
+ ### 🟡 DEPRECATED (warns only — exit code 0, 12-month grace period)
54
+
55
+ | ID | Pattern |
56
+ | --- | --- |
57
+ | `ROOTS_CAP` | `roots` capability |
58
+ | `SAMPLING_CAP` | `sampling` capability |
59
+ | `LOGGING_CAP` | `logging` capability |
60
+
61
+ ### Confidence
62
+
63
+ Every finding carries a **confidence** so you can tune signal-to-noise with `--min-confidence`:
64
+
65
+ - **high** — exact/deterministic match (session id, `-32002`, tasks methods), a structurally-verified capability (the `roots`/`sampling`/`logging` key is really *inside* a `capabilities` object), or an `initialize` string used as a method name (handler registration, `switch` case, or `req.method === 'initialize'`).
66
+ - **medium** — a `roots`/`sampling`/`logging` key/string within 5 lines of a `capabilities` mention but not structurally verified.
67
+ - **low** — a bare `'initialize'` string with no registration context.
68
+
69
+ ---
70
+
71
+ ## Before / after for each BREAKING pattern
72
+
73
+ ### 1. `Mcp-Session-Id` — sessions are removed
74
+
75
+ > *"The `Mcp-Session-Id` header and the protocol-level session that came with it are also removed."*
76
+
77
+ ```ts
78
+ // ❌ before
79
+ const sessionId = req.headers['Mcp-Session-Id'];
80
+ res.setHeader('Mcp-Session-Id', sessionId);
81
+
82
+ // ✅ after — no session header; client info & capabilities arrive in per-request _meta
83
+ function handle(req) {
84
+ const meta = req.params?._meta ?? {};
85
+ // route on meta, not on a session id
86
+ }
87
+ ```
88
+
89
+ ### 2. `initialize` / `notifications/initialized` — the handshake is removed
90
+
91
+ > *"The `initialize`/`initialized` handshake is removed. The protocol version, client info, and client capabilities that used to be exchanged once at connection time now travel in `_meta` on every request."*
92
+
93
+ ```ts
94
+ // ❌ before
95
+ server.setRequestHandler('initialize', async (req) => ({ protocolVersion, capabilities }));
96
+ server.setNotificationHandler('notifications/initialized', () => {});
97
+
98
+ // ✅ after — read the handshake data from _meta on every request
99
+ function handle(req) {
100
+ const { protocolVersion, clientInfo, capabilities } = req.params?._meta ?? {};
101
+ }
102
+ ```
103
+
104
+ ### 3. Error code `-32002` → `-32602`
105
+
106
+ > *"The error code for a missing resource changes from the MCP-custom `-32002` to the JSON-RPC standard `-32602` Invalid Params."*
107
+
108
+ ```ts
109
+ // ❌ before
110
+ return { error: { code: -32002, message: 'Resource not found' } };
111
+
112
+ // ✅ after
113
+ return { error: { code: -32602, message: 'Invalid params' } };
114
+ ```
115
+
116
+ ### 4. Legacy Tasks methods — redesigned to a handle-based lifecycle
117
+
118
+ > *"A server can answer `tools/call` with a task handle, and the client drives it with `tasks/get`, `tasks/update`, and `tasks/cancel`. Anyone who shipped against the `2025-11-25` experimental Tasks API will need to migrate to the new lifecycle."*
119
+
120
+ ```ts
121
+ // ❌ before — legacy experimental argument shapes
122
+ switch (method) {
123
+ case 'tasks/get': return getTask(id);
124
+ case 'tasks/update': return updateTask(id);
125
+ case 'tasks/cancel': return cancelTask(id);
126
+ }
127
+
128
+ // ✅ after — tools/call returns a task handle; the same method names now carry
129
+ // the NEW argument shapes. mcp-vet flags every use for manual review against
130
+ // the 2026-07-28 schema.
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Usage
136
+
137
+ ```bash
138
+ npx @booyaka/mcp-vet [paths...] # scan directories and/or files (default: current directory)
139
+ npx @booyaka/mcp-vet ./src ./packages # multiple roots
140
+ npx @booyaka/mcp-vet server.py # a single file
141
+ ```
142
+
143
+ Globs `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` and `**/*.py`, skipping `node_modules`, `.git`, `__pycache__`, `dist`, and `build`.
144
+
145
+ ### Options
146
+
147
+ | Flag | Description |
148
+ | --- | --- |
149
+ | `--github-annotations` | emit GitHub Actions `::error` / `::warning` annotations to stdout |
150
+ | `--sarif [file]` | write a SARIF 2.1.0 report (default `mcp-vet.sarif`) for GitHub code scanning |
151
+ | `--out-dir <dir>` | where to write `mcp-vet-report.md` / `mcp-vet-results.json` (default: cwd) |
152
+ | `--no-files` | don't write the markdown/json report files |
153
+ | `--only <ids>` | only run these pattern ids (comma/space separated) |
154
+ | `--disable <ids>` | skip these pattern ids |
155
+ | `--fail-on <level>` | non-zero exit on `breaking` (default), `any`, or `none` |
156
+ | `--min-confidence <level>` | report only findings at/above `high`, `medium`, or `low` (default) |
157
+ | `--ignore <glob>` | ignore paths matching a gitignore-style glob (repeatable) |
158
+ | `--max-file-size <kb>` | skip files larger than N KB (default 1536; `0` = no limit) |
159
+ | `--no-py-fallback` | disable the regex fallback used when no Python interpreter is found |
160
+ | `--config <path>` | path to a config file (see below) |
161
+ | `--color` / `--no-color` | force or disable colored output |
162
+ | `--quiet` | suppress the human-readable terminal report |
163
+ | `-v, --version` | print version |
164
+
165
+ ### Suppressing findings inline
166
+
167
+ Recognized in any comment style (`//` or `#`):
168
+
169
+ ```ts
170
+ const x = -32002; // mcp-vet-disable-line ERROR_CODE_32002
171
+ // mcp-vet-disable-next-line
172
+ const y = 'Mcp-Session-Id';
173
+ ```
174
+
175
+ - `mcp-vet-disable-line [IDS]` — suppress on the same line.
176
+ - `mcp-vet-disable-next-line [IDS]` — suppress on the following line.
177
+ - `mcp-vet-disable-file` — suppress the whole file.
178
+
179
+ Omitting the pattern ids suppresses **all** rules on that line/file; listing ids (e.g. `ERROR_CODE_32002`) suppresses only those.
180
+
181
+ ### Config file
182
+
183
+ Drop a `.mcpvetrc.json` (or `mcp-vet.config.json`) in your project root; CLI flags override it.
184
+
185
+ ```json
186
+ {
187
+ "ignore": ["**/generated/**", "vendor/"],
188
+ "disable": ["LOGGING_CAP"],
189
+ "failOn": "breaking",
190
+ "minConfidence": "medium",
191
+ "maxFileSizeKb": 2048,
192
+ "pythonFallback": true
193
+ }
194
+ ```
195
+
196
+ You can also list ignore globs one-per-line in a `.mcpvetignore` file.
197
+
198
+ ### Outputs
199
+
200
+ 1. **Terminal** — compiler-style `file:line:col`, red for BREAKING, yellow for DEPRECATED, grouped by file, with before/after snippets and a `[confidence]` tag.
201
+ 2. **`mcp-vet-report.md`** — a Markdown table (File · Line · Pattern · Severity · Confidence · Explanation).
202
+ 3. **`mcp-vet-results.json`** — a structured JSON array of every finding (line, column, confidence, docUrl, before/after, source analyzer).
203
+ 4. **`--github-annotations`** — native GitHub Actions annotations that surface inline on the PR diff.
204
+ 5. **`--sarif`** — SARIF 2.1.0 for GitHub Advanced Security "code scanning" (uploads via `github/codeql-action/upload-sarif`).
205
+
206
+ ### Exit codes
207
+
208
+ - `0` — clean, only DEPRECATED findings, or `--fail-on none`.
209
+ - `1` — findings that trip `--fail-on` (BREAKING by default).
210
+ - `2` — operational error (bad path, unreadable config, invalid flag/rule id).
211
+
212
+ ---
213
+
214
+ ## Use it in CI
215
+
216
+ ```yaml
217
+ # .github/workflows/mcp-vet.yml
218
+ name: mcp-vet
219
+ on: [push, pull_request]
220
+ jobs:
221
+ vet:
222
+ runs-on: ubuntu-latest
223
+ steps:
224
+ - uses: actions/checkout@v4
225
+ - uses: actions/setup-node@v4
226
+ with: { node-version: '20' }
227
+ - run: npx @booyaka/mcp-vet . --github-annotations
228
+ ```
229
+
230
+ `setup-node` runners already include Python 3, which `mcp-vet` uses to scan `.py` files. If no interpreter is found, it automatically falls back to a regex scanner (reduced precision) unless you pass `--no-py-fallback`; TypeScript/JavaScript scanning is unaffected either way.
231
+
232
+ To upload results to GitHub code scanning instead:
233
+
234
+ ```yaml
235
+ - run: npx @booyaka/mcp-vet . --sarif mcp-vet.sarif --fail-on none
236
+ - uses: github/codeql-action/upload-sarif@v3
237
+ with: { sarif_file: mcp-vet.sarif }
238
+ ```
239
+
240
+ ---
241
+
242
+ ## How it works
243
+
244
+ - **TypeScript / JavaScript** — parsed with [`ts-morph`](https://ts-morph.com); the analyzer walks the AST and emits normalized tokens (string literals, signed numeric literals, identifiers, object keys) annotated with structural capability context and registration context.
245
+ - **Python** — a bundled script (`dist/python/mcp_ast_scan.py`) runs `ast.parse` + a context-tracking walk in a subprocess (chunked for large repos) and emits the same token shape. When no interpreter exists, a regex fallback covers the deterministic rules.
246
+ - A single rule engine applies all 7 rules to those tokens, so TS and Python behave identically. Findings are de-duplicated per (line, column, rule) and can be suppressed inline.
247
+
248
+ ## Requirements
249
+
250
+ - Node.js ≥ 18
251
+ - Python 3 (optional — only needed for full-precision `.py` scanning; `python`, `py`, or `python3` on `PATH`)
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ npm install # installs deps and builds (via prepare)
257
+ npm run build # tsc -> dist/ + copies the Python script
258
+ npm test # builds, then runs the Node.js built-in test runner (18 tests)
259
+ ```
260
+
261
+ 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).
262
+
263
+ ## License
264
+
265
+ MIT — see [LICENSE](./LICENSE).
package/dist/cli.js ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("node:fs"));
38
+ const path = __importStar(require("node:path"));
39
+ const commander_1 = require("commander");
40
+ const scanner_1 = require("./scanner");
41
+ const ignore_1 = require("./ignore");
42
+ const config_1 = require("./config");
43
+ const types_1 = require("./types");
44
+ const constants_1 = require("./constants");
45
+ const reporters_1 = require("./reporters");
46
+ const CONF_VALUES = ['high', 'medium', 'low'];
47
+ const FAILON_VALUES = ['breaking', 'any', 'none'];
48
+ function fail(msg) {
49
+ console.error(`mcp-vet: ${msg}`);
50
+ process.exit(2);
51
+ }
52
+ function parsePatternIds(raw) {
53
+ if (!raw)
54
+ return undefined;
55
+ const parts = raw
56
+ .toUpperCase()
57
+ .split(/[\s,]+/)
58
+ .filter(Boolean);
59
+ const ids = [];
60
+ for (const p of parts) {
61
+ if (!types_1.ALL_PATTERN_IDS.includes(p)) {
62
+ fail(`unknown pattern id "${p}". Valid ids: ${types_1.ALL_PATTERN_IDS.join(', ')}`);
63
+ }
64
+ ids.push(p);
65
+ }
66
+ return ids;
67
+ }
68
+ function readIgnoreFile(dir) {
69
+ const p = path.join(dir, '.mcpvetignore');
70
+ try {
71
+ return fs
72
+ .readFileSync(p, 'utf8')
73
+ .split(/\r?\n/)
74
+ .map((l) => l.trim())
75
+ .filter((l) => l && !l.startsWith('#'));
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ }
81
+ const program = new commander_1.Command();
82
+ program
83
+ .name('mcp-vet')
84
+ .description('Scan MCP server source code for patterns that break under the 2026-07-28 MCP spec release candidate.')
85
+ .argument('[paths...]', 'files or directories to scan', ['.'])
86
+ .option('--github-annotations', 'emit GitHub Actions ::error/::warning annotations to stdout')
87
+ .option('--sarif [file]', 'write a SARIF 2.1.0 report (default file: mcp-vet.sarif)')
88
+ .option('--out-dir <dir>', 'directory for mcp-vet-report.md and mcp-vet-results.json', process.cwd())
89
+ .option('--no-files', 'do not write the markdown/json report files')
90
+ .option('--only <ids>', 'only run these pattern ids (comma/space separated)')
91
+ .option('--disable <ids>', 'skip these pattern ids (comma/space separated)')
92
+ .option('--fail-on <level>', `exit non-zero on: ${FAILON_VALUES.join(' | ')}`, 'breaking')
93
+ .option('--min-confidence <level>', `report only findings at/above: ${CONF_VALUES.join(' | ')}`, 'low')
94
+ .option('--ignore <glob>', 'ignore paths matching glob (repeatable)', (v, acc) => {
95
+ acc.push(v);
96
+ return acc;
97
+ }, [])
98
+ .option('--max-file-size <kb>', 'skip files larger than this many KB (0 = no limit)', '1536')
99
+ .option('--no-py-fallback', 'disable the regex fallback when no Python interpreter is found')
100
+ .option('--config <path>', 'path to a config file (.mcpvetrc.json)')
101
+ .option('--color', 'force colored output')
102
+ .option('--no-color', 'disable colored output')
103
+ .option('--quiet', 'suppress the human-readable terminal report')
104
+ .version((0, constants_1.getVersion)(), '-v, --version')
105
+ .showHelpAfterError();
106
+ program.parse(process.argv);
107
+ const opts = program.opts();
108
+ const paths = program.args.length ? program.args : ['.'];
109
+ // --- Resolve configuration (CLI over config file over defaults) ---
110
+ let config = {};
111
+ try {
112
+ config = (0, config_1.loadConfig)(process.cwd(), opts.config);
113
+ }
114
+ catch (err) {
115
+ if (err instanceof config_1.ConfigError)
116
+ fail(err.message);
117
+ throw err;
118
+ }
119
+ // Validate enums
120
+ if (!FAILON_VALUES.includes(opts.failOn)) {
121
+ fail(`invalid --fail-on "${opts.failOn}". Valid: ${FAILON_VALUES.join(', ')}`);
122
+ }
123
+ if (!CONF_VALUES.includes(opts.minConfidence)) {
124
+ fail(`invalid --min-confidence "${opts.minConfidence}". Valid: ${CONF_VALUES.join(', ')}`);
125
+ }
126
+ // CLI value wins when explicitly set; otherwise fall back to the config file.
127
+ const fromCli = (key) => program.getOptionValueSource(key) === 'cli';
128
+ const failOn = fromCli('failOn')
129
+ ? opts.failOn
130
+ : config.failOn ?? opts.failOn;
131
+ const minConfidence = fromCli('minConfidence')
132
+ ? opts.minConfidence
133
+ : config.minConfidence ?? opts.minConfidence;
134
+ const normalizeIds = (ids) => ids
135
+ ?.map((s) => String(s).toUpperCase())
136
+ .filter((s) => types_1.ALL_PATTERN_IDS.includes(s));
137
+ const cliOnly = parsePatternIds(opts.only);
138
+ const cliDisable = parsePatternIds(opts.disable);
139
+ const only = cliOnly ?? normalizeIds(config.only);
140
+ const disable = cliDisable ?? normalizeIds(config.disable);
141
+ let enabled = new Set(types_1.ALL_PATTERN_IDS);
142
+ if (only && only.length)
143
+ enabled = new Set(only.filter((id) => types_1.ALL_PATTERN_IDS.includes(id)));
144
+ else if (disable && disable.length) {
145
+ for (const id of disable)
146
+ enabled.delete(id);
147
+ }
148
+ if (enabled.size === 0)
149
+ fail('no rules enabled after applying --only/--disable.');
150
+ const maxKbRaw = Number(opts.maxFileSize);
151
+ if (!Number.isFinite(maxKbRaw) || maxKbRaw < 0)
152
+ fail(`invalid --max-file-size "${opts.maxFileSize}".`);
153
+ const maxFileSizeKb = fromCli('maxFileSize')
154
+ ? maxKbRaw
155
+ : config.maxFileSizeKb != null
156
+ ? config.maxFileSizeKb
157
+ : maxKbRaw;
158
+ const pythonFallback = fromCli('pyFallback')
159
+ ? opts.pyFallback
160
+ : config.pythonFallback != null
161
+ ? config.pythonFallback
162
+ : opts.pyFallback;
163
+ const color = fromCli('color') ? opts.color : undefined;
164
+ // Ignore patterns: config + CLI + .mcpvetignore in cwd and each root dir
165
+ const ignorePatterns = new Set([...(config.ignore ?? []), ...opts.ignore]);
166
+ for (const line of readIgnoreFile(process.cwd()))
167
+ ignorePatterns.add(line);
168
+ for (const p of paths) {
169
+ try {
170
+ const abs = path.resolve(p);
171
+ if (fs.statSync(abs).isDirectory()) {
172
+ for (const line of readIgnoreFile(abs))
173
+ ignorePatterns.add(line);
174
+ }
175
+ }
176
+ catch {
177
+ /* validated later in scan() */
178
+ }
179
+ }
180
+ const ignore = new ignore_1.IgnoreMatcher([...ignorePatterns]);
181
+ // --- Scan ---
182
+ let result;
183
+ try {
184
+ result = (0, scanner_1.scan)(paths, {
185
+ enabled,
186
+ ignore,
187
+ maxFileSizeKb,
188
+ pythonFallback,
189
+ minConfidence,
190
+ });
191
+ }
192
+ catch (err) {
193
+ if (err instanceof scanner_1.ScanError)
194
+ fail(err.message);
195
+ throw err;
196
+ }
197
+ // --- Report ---
198
+ if (opts.githubAnnotations) {
199
+ (0, reporters_1.printGithubAnnotations)(result.findings);
200
+ }
201
+ if (!opts.quiet) {
202
+ (0, reporters_1.reportTerminal)(result, { color });
203
+ }
204
+ if (opts.sarif) {
205
+ const sarifPath = path.resolve(process.cwd(), typeof opts.sarif === 'string' ? opts.sarif : 'mcp-vet.sarif');
206
+ try {
207
+ (0, reporters_1.writeSarif)(result, sarifPath);
208
+ if (!opts.quiet)
209
+ console.log(`Wrote ${sarifPath}`);
210
+ }
211
+ catch (err) {
212
+ console.error(`mcp-vet: failed to write SARIF: ${err.message}`);
213
+ }
214
+ }
215
+ if (opts.files) {
216
+ try {
217
+ const md = (0, reporters_1.writeMarkdown)(result, opts.outDir);
218
+ const json = (0, reporters_1.writeJson)(result, opts.outDir);
219
+ if (!opts.quiet) {
220
+ console.log(`Wrote ${md}`);
221
+ console.log(`Wrote ${json}`);
222
+ }
223
+ }
224
+ catch (err) {
225
+ console.error(`mcp-vet: failed to write report files: ${err.message}`);
226
+ }
227
+ }
228
+ // --- Exit code ---
229
+ const hasBreaking = result.findings.some((f) => f.severity === 'BREAKING');
230
+ const hasAny = result.findings.length > 0;
231
+ let failing = false;
232
+ if (failOn === 'breaking')
233
+ failing = hasBreaking;
234
+ else if (failOn === 'any')
235
+ failing = hasAny;
236
+ else
237
+ failing = false; // 'none'
238
+ process.exit(failing ? 1 : 0);
package/dist/config.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ConfigError = void 0;
37
+ exports.loadConfig = loadConfig;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const CONFIG_NAMES = ['.mcpvetrc.json', 'mcp-vet.config.json'];
41
+ class ConfigError extends Error {
42
+ }
43
+ exports.ConfigError = ConfigError;
44
+ /**
45
+ * Load config. If `explicitPath` is given it must exist and parse (throws
46
+ * ConfigError otherwise). Otherwise the first known config file found in `cwd`
47
+ * is used; missing config is not an error.
48
+ */
49
+ function loadConfig(cwd, explicitPath) {
50
+ let file;
51
+ if (explicitPath) {
52
+ file = path.resolve(cwd, explicitPath);
53
+ if (!fs.existsSync(file)) {
54
+ throw new ConfigError(`config file not found: ${explicitPath}`);
55
+ }
56
+ }
57
+ else {
58
+ for (const name of CONFIG_NAMES) {
59
+ const candidate = path.join(cwd, name);
60
+ if (fs.existsSync(candidate)) {
61
+ file = candidate;
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ if (!file)
67
+ return {};
68
+ let raw;
69
+ try {
70
+ raw = JSON.parse(fs.readFileSync(file, 'utf8'));
71
+ }
72
+ catch (err) {
73
+ throw new ConfigError(`failed to parse ${file}: ${err.message}`);
74
+ }
75
+ if (typeof raw !== 'object' || raw === null) {
76
+ throw new ConfigError(`config must be a JSON object: ${file}`);
77
+ }
78
+ return raw;
79
+ }