@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 +54 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/cli.js +238 -0
- package/dist/config.js +79 -0
- package/dist/constants.js +60 -0
- package/dist/ignore.js +66 -0
- package/dist/py-analyzer.js +114 -0
- package/dist/py-fallback.js +44 -0
- package/dist/python/mcp_ast_scan.py +183 -0
- package/dist/reporters.js +253 -0
- package/dist/rules.js +166 -0
- package/dist/scanner.js +235 -0
- package/dist/suppress.js +66 -0
- package/dist/ts-analyzer.js +216 -0
- package/dist/types.js +12 -0
- package/package.json +60 -0
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
|
+
[](https://www.npmjs.com/package/@booyaka/mcp-vet)
|
|
4
|
+
[](https://github.com/Booyaka101/mcp-vet/actions/workflows/ci.yml)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](./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
|
+
}
|