mcp-lint 0.2.0 → 0.4.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/README.md +322 -50
- package/dist/cli/commands/check.js +98 -52
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/compat.js +12 -2
- package/dist/cli/commands/compat.js.map +1 -1
- package/dist/cli/commands/diff.d.ts +2 -0
- package/dist/cli/commands/diff.js +134 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/fix.js +3 -1
- package/dist/cli/commands/fix.js.map +1 -1
- package/dist/cli/commands/preflight.d.ts +2 -0
- package/dist/cli/commands/preflight.js +109 -0
- package/dist/cli/commands/preflight.js.map +1 -0
- package/dist/config/config.d.ts +1 -1
- package/dist/config/config.js +2 -2
- package/dist/config/config.js.map +1 -1
- package/dist/config/plugin-loader.d.ts +6 -0
- package/dist/config/plugin-loader.js +19 -0
- package/dist/config/plugin-loader.js.map +1 -0
- package/dist/config/presets.d.ts +1 -1
- package/dist/config/presets.js +13 -4
- package/dist/config/presets.js.map +1 -1
- package/dist/core/quality-score.d.ts +15 -0
- package/dist/core/quality-score.js +30 -0
- package/dist/core/quality-score.js.map +1 -0
- package/dist/core/rule.d.ts +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/preflight/engine.d.ts +2 -0
- package/dist/preflight/engine.js +73 -0
- package/dist/preflight/engine.js.map +1 -0
- package/dist/preflight/index.d.ts +4 -0
- package/dist/preflight/index.js +4 -0
- package/dist/preflight/index.js.map +1 -0
- package/dist/preflight/policy.d.ts +4 -0
- package/dist/preflight/policy.js +97 -0
- package/dist/preflight/policy.js.map +1 -0
- package/dist/preflight/scorer.d.ts +7 -0
- package/dist/preflight/scorer.js +121 -0
- package/dist/preflight/scorer.js.map +1 -0
- package/dist/preflight/types.d.ts +57 -0
- package/dist/preflight/types.js +2 -0
- package/dist/preflight/types.js.map +1 -0
- package/examples/actions/destructive-delete.json +11 -0
- package/examples/actions/rewrite-tmp.json +8 -0
- package/examples/actions/safe-read.json +8 -0
- package/examples/github-action.yml +23 -0
- package/examples/policies/default.yml +59 -0
- package/examples/policies/permissive-dev.yml +33 -0
- package/examples/policies/strict.yml +42 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/mcp-lint)
|
|
4
4
|
[](https://www.npmjs.com/package/mcp-lint)
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://nodejs.org)
|
|
7
7
|
|
|
8
8
|
**Lint MCP server tool schemas for cross-client compatibility.**
|
|
9
9
|
|
|
10
|
-
MCP servers expose tool schemas consumed by **Claude**, **Cursor**, **Gemini**,
|
|
10
|
+
MCP servers expose tool schemas consumed by **Claude**, **Cursor**, **Gemini**, **VS Code Copilot**, **Windsurf**, **Cline**, **OpenAI Agents SDK**, and **Continue.dev**. Each client has different JSON Schema support and quirks — a schema that works in Claude may silently break in Cursor or OpenAI. `mcp-lint` catches these issues before your users do.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
$ npx mcp-lint check tools.json
|
|
@@ -41,21 +41,80 @@ npm install --save-dev mcp-lint
|
|
|
41
41
|
## Quick Start
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
# Generate a config file
|
|
45
|
-
mcp-lint init
|
|
46
|
-
|
|
47
44
|
# Check a JSON or YAML file
|
|
48
45
|
mcp-lint check tools.json
|
|
49
|
-
mcp-lint check tools.yaml
|
|
50
46
|
|
|
51
|
-
#
|
|
52
|
-
mcp-lint
|
|
47
|
+
# See compatibility across all clients at a glance
|
|
48
|
+
mcp-lint compat tools.json
|
|
53
49
|
|
|
54
|
-
#
|
|
55
|
-
mcp-lint
|
|
50
|
+
# Learn what a rule does
|
|
51
|
+
mcp-lint explain openai/no-additional-properties
|
|
56
52
|
|
|
57
53
|
# Auto-fix what can be fixed
|
|
58
54
|
mcp-lint fix tools.json --in-place
|
|
55
|
+
|
|
56
|
+
# Generate a config file
|
|
57
|
+
mcp-lint init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## `mcp-lint compat` — Compatibility Matrix
|
|
63
|
+
|
|
64
|
+
See exactly which tools work with which clients:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
$ mcp-lint compat tools.json
|
|
68
|
+
|
|
69
|
+
claude cursor gemini vscode windsurf cline openai continue
|
|
70
|
+
search-tool ✓ ✗ ✓ ✓ ✓ ✓ ✓ ✓
|
|
71
|
+
file-reader ✓ ✓ ✗ ✓ ✓ ✓ ✓ ✗
|
|
72
|
+
database-query ✓ ✓ ✓ ✓ ✓ ✓ ✗ ✓
|
|
73
|
+
|
|
74
|
+
2 tools have compatibility issues. Run `mcp-lint check` for details.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
mcp-lint compat tools.json --format markdown # for PRs and GitHub Step Summary
|
|
79
|
+
mcp-lint compat tools.json --format json # machine-readable
|
|
80
|
+
mcp-lint compat tools.json --client openai # single client column
|
|
81
|
+
mcp-lint compat --server stdio -- node server.js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## `mcp-lint explain` — Rule Documentation
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
$ mcp-lint explain openai/no-additional-properties
|
|
90
|
+
|
|
91
|
+
Rule: openai/no-additional-properties
|
|
92
|
+
Severity: error
|
|
93
|
+
Clients: openai
|
|
94
|
+
Fixable: yes
|
|
95
|
+
|
|
96
|
+
Description
|
|
97
|
+
OpenAI Agents SDK strict mode requires `additionalProperties: false` on the root inputSchema.
|
|
98
|
+
|
|
99
|
+
Why
|
|
100
|
+
OpenAI's strict function calling mode rejects schemas that don't explicitly set
|
|
101
|
+
`additionalProperties: false` at the root level.
|
|
102
|
+
|
|
103
|
+
Bad example
|
|
104
|
+
{
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": { "q": { "type": "string" } }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Good example
|
|
110
|
+
{
|
|
111
|
+
"type": "object",
|
|
112
|
+
"properties": { "q": { "type": "string" } },
|
|
113
|
+
"additionalProperties": false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Auto-fix
|
|
117
|
+
Add `"additionalProperties": false` to the root inputSchema.
|
|
59
118
|
```
|
|
60
119
|
|
|
61
120
|
---
|
|
@@ -73,15 +132,21 @@ mcp-lint check tools.json --format json # machine-readable (CI/CD)
|
|
|
73
132
|
mcp-lint check tools.json --format markdown # for PRs and GitHub summaries
|
|
74
133
|
|
|
75
134
|
# Filters
|
|
76
|
-
mcp-lint check tools.json --clients claude,cursor
|
|
77
|
-
mcp-lint check tools.json --severity error
|
|
78
|
-
mcp-lint check tools.json --quiet
|
|
135
|
+
mcp-lint check tools.json --clients claude,cursor,openai # only these clients' rules
|
|
136
|
+
mcp-lint check tools.json --severity error # errors only
|
|
137
|
+
mcp-lint check tools.json --quiet # same as --severity error
|
|
79
138
|
mcp-lint check tools.json --ignore "debug-tool,internal-tool"
|
|
80
139
|
|
|
81
140
|
# Config
|
|
82
141
|
mcp-lint check tools.json --config path/to/.mcplintrc.json
|
|
83
142
|
mcp-lint check tools.json --no-color
|
|
84
143
|
|
|
144
|
+
# Quality score (0–100 per tool with A-F grades)
|
|
145
|
+
mcp-lint check tools.json --score
|
|
146
|
+
|
|
147
|
+
# Watch mode (re-lints on file change)
|
|
148
|
+
mcp-lint check tools.json --watch
|
|
149
|
+
|
|
85
150
|
# Live servers
|
|
86
151
|
mcp-lint check --server stdio -- node my-server.js
|
|
87
152
|
mcp-lint check --server stdio -- python my_server.py arg1 arg2
|
|
@@ -98,6 +163,20 @@ mcp-lint check --server sse --url http://localhost:3000/sse
|
|
|
98
163
|
|
|
99
164
|
---
|
|
100
165
|
|
|
166
|
+
### `mcp-lint diff <before> <after>`
|
|
167
|
+
|
|
168
|
+
Compare lint results between two versions of your schema — useful in CI to catch regressions.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
mcp-lint diff tools-v1.json tools-v2.json
|
|
172
|
+
mcp-lint diff tools-v1.json tools-v2.json --format markdown >> $GITHUB_STEP_SUMMARY
|
|
173
|
+
mcp-lint diff tools-v1.json tools-v2.json --format json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Exit codes:** `0` = no new errors introduced, `1` = new errors found, `2` = error
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
101
180
|
### `mcp-lint fix [input]`
|
|
102
181
|
|
|
103
182
|
Auto-fixes issues that are safe to fix automatically (no semantic changes).
|
|
@@ -163,6 +242,31 @@ mcp-lint init --force # overwrite existing
|
|
|
163
242
|
|------|----------|---------|-------------|
|
|
164
243
|
| `vscode/max-params` | 🟡 warning | ❌ | VS Code Copilot performance degrades with more than 15 parameters |
|
|
165
244
|
|
|
245
|
+
### Windsurf-specific
|
|
246
|
+
|
|
247
|
+
| Rule | Severity | Fixable | Description |
|
|
248
|
+
|------|----------|---------|-------------|
|
|
249
|
+
| `windsurf/no-union-types` | 🟡 warning | ❌ | `anyOf` with more than 2 variants not supported |
|
|
250
|
+
|
|
251
|
+
### Cline-specific
|
|
252
|
+
|
|
253
|
+
| Rule | Severity | Fixable | Description |
|
|
254
|
+
|------|----------|---------|-------------|
|
|
255
|
+
| `cline/description-max-length` | 🟡 warning | ✅ | Descriptions over 200 characters are truncated by Cline |
|
|
256
|
+
|
|
257
|
+
### OpenAI Agents SDK-specific
|
|
258
|
+
|
|
259
|
+
| Rule | Severity | Fixable | Description |
|
|
260
|
+
|------|----------|---------|-------------|
|
|
261
|
+
| `openai/no-additional-properties` | 🔴 error | ✅ | Strict mode requires `additionalProperties: false` on root inputSchema |
|
|
262
|
+
| `openai/strict-types` | 🔴 error | ❌ | Only `string`, `number`, `boolean`, `object`, `array`, `null` allowed |
|
|
263
|
+
|
|
264
|
+
### Continue.dev-specific
|
|
265
|
+
|
|
266
|
+
| Rule | Severity | Fixable | Description |
|
|
267
|
+
|------|----------|---------|-------------|
|
|
268
|
+
| `continue/no-default-values` | 🟡 warning | ❌ | Continue.dev ignores `default` fields — document defaults in descriptions instead |
|
|
269
|
+
|
|
166
270
|
---
|
|
167
271
|
|
|
168
272
|
## Configuration
|
|
@@ -171,29 +275,79 @@ mcp-lint init --force # overwrite existing
|
|
|
171
275
|
|
|
172
276
|
```json
|
|
173
277
|
{
|
|
174
|
-
"$schema": "https://raw.githubusercontent.com/robert19001-cmyk/mcp-lint/main/config-schema.json",
|
|
175
278
|
"rules": {
|
|
176
279
|
"no-required-false": "error",
|
|
177
280
|
"description-exists": "warning",
|
|
178
281
|
"max-depth": "off",
|
|
179
282
|
"claude/no-type-array": "error"
|
|
180
283
|
},
|
|
181
|
-
"clients": ["claude", "cursor", "gemini", "vscode"],
|
|
284
|
+
"clients": ["claude", "cursor", "gemini", "vscode", "windsurf", "cline", "openai", "continue"],
|
|
182
285
|
"ignore": ["internal-debug-tool"],
|
|
183
286
|
"maxDepth": 5
|
|
184
287
|
}
|
|
185
288
|
```
|
|
186
289
|
|
|
290
|
+
### Presets
|
|
291
|
+
|
|
292
|
+
Use `"extends"` to start from a built-in preset:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{ "extends": "recommended" }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
"extends": "strict",
|
|
301
|
+
"rules": {
|
|
302
|
+
"description-exists": "warning"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
| Preset | Description |
|
|
308
|
+
|--------|-------------|
|
|
309
|
+
| `recommended` | All 8 clients enabled, default severities |
|
|
310
|
+
| `strict` | All rules set to `error`, `maxDepth: 3` |
|
|
311
|
+
|
|
187
312
|
Rule severities: `"error"` | `"warning"` | `"info"` | `"off"`
|
|
188
313
|
|
|
189
314
|
---
|
|
190
315
|
|
|
191
316
|
## CI/CD — GitHub Actions
|
|
192
317
|
|
|
318
|
+
### Using the official Action
|
|
319
|
+
|
|
193
320
|
```yaml
|
|
194
321
|
name: MCP Schema Lint
|
|
195
322
|
on: [push, pull_request]
|
|
196
323
|
|
|
324
|
+
jobs:
|
|
325
|
+
lint:
|
|
326
|
+
runs-on: ubuntu-latest
|
|
327
|
+
steps:
|
|
328
|
+
- uses: actions/checkout@v4
|
|
329
|
+
- uses: robert19001-cmyk/mcp-lint@v0.3.0
|
|
330
|
+
with:
|
|
331
|
+
input: tools.json
|
|
332
|
+
severity: warning
|
|
333
|
+
score: 'true'
|
|
334
|
+
fail_on: error
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Action inputs:**
|
|
338
|
+
|
|
339
|
+
| Input | Default | Description |
|
|
340
|
+
|-------|---------|-------------|
|
|
341
|
+
| `input` | _(required)_ | Path to MCP tools JSON file |
|
|
342
|
+
| `format` | `terminal` | `terminal\|json\|markdown` |
|
|
343
|
+
| `severity` | `info` | Minimum severity to report |
|
|
344
|
+
| `clients` | all | Comma-separated client filter |
|
|
345
|
+
| `fail_on` | `error` | Fail CI when issues at this severity found |
|
|
346
|
+
| `score` | `false` | Show quality score |
|
|
347
|
+
|
|
348
|
+
### Manual workflow
|
|
349
|
+
|
|
350
|
+
```yaml
|
|
197
351
|
jobs:
|
|
198
352
|
lint:
|
|
199
353
|
runs-on: ubuntu-latest
|
|
@@ -205,17 +359,61 @@ jobs:
|
|
|
205
359
|
# Fail the build on errors
|
|
206
360
|
- run: npx mcp-lint check ./src/tools.json
|
|
207
361
|
|
|
208
|
-
# Post
|
|
209
|
-
- run: npx mcp-lint
|
|
362
|
+
# Post compatibility matrix to PR summary
|
|
363
|
+
- run: npx mcp-lint compat ./src/tools.json --format markdown >> $GITHUB_STEP_SUMMARY
|
|
210
364
|
if: always()
|
|
211
365
|
|
|
212
|
-
#
|
|
213
|
-
- run: npx mcp-lint
|
|
214
|
-
- uses: actions/upload-artifact@v4
|
|
366
|
+
# Diff against previous version
|
|
367
|
+
- run: npx mcp-lint diff tools-before.json tools-after.json --format markdown >> $GITHUB_STEP_SUMMARY
|
|
215
368
|
if: always()
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Plugin API
|
|
374
|
+
|
|
375
|
+
Share and reuse rule sets via npm packages.
|
|
376
|
+
|
|
377
|
+
### Using a plugin
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npm install mcp-lint-config-nextjs
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"plugins": ["mcp-lint-config-nextjs"],
|
|
386
|
+
"extends": "recommended"
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Writing a plugin
|
|
391
|
+
|
|
392
|
+
A plugin is an npm package that exports a `rules` array of `Rule` objects:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// mcp-lint-config-myserver/index.ts
|
|
396
|
+
import type { Rule } from 'mcp-lint';
|
|
397
|
+
|
|
398
|
+
const myRule: Rule = {
|
|
399
|
+
id: 'myserver/no-large-params',
|
|
400
|
+
severity: 'warning',
|
|
401
|
+
description: 'My server limits param count to 10',
|
|
402
|
+
clients: ['claude', 'cursor'],
|
|
403
|
+
check(tool) {
|
|
404
|
+
const count = Object.keys(tool.inputSchema.properties ?? {}).length;
|
|
405
|
+
if (count > 10) return [{
|
|
406
|
+
toolName: tool.name,
|
|
407
|
+
ruleId: 'myserver/no-large-params',
|
|
408
|
+
severity: 'warning',
|
|
409
|
+
message: `Too many params (${count})`,
|
|
410
|
+
path: 'inputSchema.properties',
|
|
411
|
+
}];
|
|
412
|
+
return [];
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
export const rules = [myRule];
|
|
219
417
|
```
|
|
220
418
|
|
|
221
419
|
---
|
|
@@ -231,7 +429,7 @@ import { applyFixes } from 'mcp-lint/fixer';
|
|
|
231
429
|
// Check
|
|
232
430
|
const tools = await loadFile('tools.json');
|
|
233
431
|
const engine = new LintEngine(allRules, {
|
|
234
|
-
clients: ['claude', 'cursor'],
|
|
432
|
+
clients: ['claude', 'cursor', 'openai'],
|
|
235
433
|
ignore: ['debug-tool'],
|
|
236
434
|
});
|
|
237
435
|
const diagnostics = engine.lint(tools);
|
|
@@ -242,39 +440,113 @@ const fixed = applyFixes(tools, diagnostics);
|
|
|
242
440
|
|
|
243
441
|
---
|
|
244
442
|
|
|
245
|
-
##
|
|
443
|
+
## `mcp-lint preflight` — Runtime Decision Firewall
|
|
444
|
+
|
|
445
|
+
**Lint catches bad schemas. Preflight catches bad actions.**
|
|
446
|
+
|
|
447
|
+
At runtime, your agent is about to call a tool — delete a file, send a request, charge a card. Preflight sits between the agent and the action and answers:
|
|
246
448
|
|
|
247
|
-
|
|
449
|
+
- Is it allowed?
|
|
450
|
+
- How risky is it? (0.0–1.0 deterministic score)
|
|
451
|
+
- Is it reversible?
|
|
452
|
+
- Does it need human approval?
|
|
453
|
+
- Is there a safer alternative?
|
|
454
|
+
|
|
455
|
+
```bash
|
|
456
|
+
$ mcp-lint preflight action.json --policy preflight.yml
|
|
457
|
+
|
|
458
|
+
Preflight Decision
|
|
459
|
+
────────────────────────────────────────
|
|
460
|
+
Decision: DENY
|
|
461
|
+
Risk score: 1.00
|
|
462
|
+
Reversibility: irreversible
|
|
463
|
+
Policies: block-destructive-prod
|
|
464
|
+
Reasons: base_shell, destructive_pattern, sensitive_target, irreversible_operation
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Action format
|
|
468
|
+
|
|
469
|
+
```json
|
|
470
|
+
{
|
|
471
|
+
"tool_type": "shell",
|
|
472
|
+
"tool_name": "bash",
|
|
473
|
+
"action": "rm -rf /prod/data",
|
|
474
|
+
"target": "/prod/data",
|
|
475
|
+
"context": { "environment": "prod" }
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Policy file (YAML)
|
|
480
|
+
|
|
481
|
+
```yaml
|
|
482
|
+
version: 1
|
|
483
|
+
defaults:
|
|
484
|
+
approval_threshold: 0.70
|
|
485
|
+
block_threshold: 0.92
|
|
486
|
+
|
|
487
|
+
rules:
|
|
488
|
+
- id: block-prod-delete
|
|
489
|
+
when:
|
|
490
|
+
tool_type: shell
|
|
491
|
+
action_matches: ["rm -rf"]
|
|
492
|
+
target_matches: ["/prod", "/etc"]
|
|
493
|
+
effect: deny
|
|
494
|
+
|
|
495
|
+
- id: rewrite-tmp-delete
|
|
496
|
+
when:
|
|
497
|
+
tool_type: shell
|
|
498
|
+
action_matches: ["rm -rf ./tmp"]
|
|
499
|
+
effect: allow_with_rewrite
|
|
500
|
+
rewrite:
|
|
501
|
+
tool_type: file_delete
|
|
502
|
+
action: move_to_trash
|
|
503
|
+
target: ./tmp
|
|
504
|
+
|
|
505
|
+
- id: approval-payments
|
|
506
|
+
when:
|
|
507
|
+
tool_type: payment
|
|
508
|
+
effect: require_approval
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Sample policies in `examples/policies/`: `default.yml`, `strict.yml`, `permissive-dev.yml`.
|
|
512
|
+
|
|
513
|
+
### SDK — embed in your MCP server or agent runtime
|
|
248
514
|
|
|
249
515
|
```typescript
|
|
250
|
-
import
|
|
516
|
+
import { preflight, loadPolicy } from 'mcp-lint/preflight';
|
|
251
517
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (someCondition(tool.inputSchema)) {
|
|
262
|
-
diagnostics.push({
|
|
263
|
-
ruleId: 'my-rule',
|
|
264
|
-
severity: 'warning',
|
|
265
|
-
message: 'Human-readable explanation',
|
|
266
|
-
toolName: tool.name,
|
|
267
|
-
path: 'inputSchema.properties.something',
|
|
268
|
-
clients: this.clients,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return diagnostics;
|
|
518
|
+
const policy = await loadPolicy('./preflight.yml');
|
|
519
|
+
|
|
520
|
+
const decision = preflight(
|
|
521
|
+
{
|
|
522
|
+
tool_type: 'shell',
|
|
523
|
+
tool_name: 'bash',
|
|
524
|
+
action: 'rm -rf /tmp',
|
|
525
|
+
target: '/tmp',
|
|
273
526
|
},
|
|
274
|
-
|
|
527
|
+
policy,
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
if (decision.decision === 'deny') {
|
|
531
|
+
throw new Error(`Blocked: ${decision.reasons.join(', ')}`);
|
|
532
|
+
}
|
|
533
|
+
if (decision.decision === 'require_approval') {
|
|
534
|
+
await askUser(decision);
|
|
535
|
+
}
|
|
536
|
+
if (decision.decision === 'allow_with_rewrite') {
|
|
537
|
+
return executeSafer(decision.safe_alternative);
|
|
538
|
+
}
|
|
275
539
|
```
|
|
276
540
|
|
|
277
|
-
|
|
541
|
+
### Exit codes
|
|
542
|
+
|
|
543
|
+
| Code | Meaning |
|
|
544
|
+
|------|---------|
|
|
545
|
+
| `0` | allow / allow_with_rewrite |
|
|
546
|
+
| `1` | require_approval |
|
|
547
|
+
| `2` | deny / error |
|
|
548
|
+
|
|
549
|
+
Use them directly in shell wrappers or CI gates.
|
|
278
550
|
|
|
279
551
|
---
|
|
280
552
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
1
2
|
import { loadFile } from '../../loaders/file-loader.js';
|
|
2
3
|
import { loadStdio } from '../../loaders/stdio-loader.js';
|
|
3
4
|
import { loadSSE } from '../../loaders/sse-loader.js';
|
|
@@ -7,6 +8,10 @@ import { formatTerminal } from '../formatters/terminal.js';
|
|
|
7
8
|
import { formatJson } from '../formatters/json.js';
|
|
8
9
|
import { formatMarkdown } from '../formatters/markdown.js';
|
|
9
10
|
import { loadConfig } from '../../config/config.js';
|
|
11
|
+
import { loadPluginRules } from '../../config/plugin-loader.js';
|
|
12
|
+
import { writeFile } from 'fs/promises';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { computeQualityReport } from '../../core/quality-score.js';
|
|
10
15
|
const SEVERITY_ORDER = { error: 3, warning: 2, info: 1 };
|
|
11
16
|
function filterBySeverity(diagnostics, min) {
|
|
12
17
|
return diagnostics.filter((d) => SEVERITY_ORDER[d.severity] >= SEVERITY_ORDER[min]);
|
|
@@ -24,65 +29,106 @@ export function registerCheckCommand(program) {
|
|
|
24
29
|
.option('--quiet', 'Only output errors (no warnings/info)')
|
|
25
30
|
.option('--server <type>', 'Server transport: stdio|sse')
|
|
26
31
|
.option('--url <url>', 'SSE server URL (use with --server sse)')
|
|
32
|
+
.option('--output-file <path>', 'Write results to a file instead of stdout')
|
|
33
|
+
.option('--watch', 'Re-lint on file change (only works with file input)')
|
|
34
|
+
.option('--score', 'Show quality score (0–100) per tool')
|
|
27
35
|
.allowExcessArguments(true)
|
|
28
36
|
.action(async (input, options) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
async function runLint() {
|
|
38
|
+
try {
|
|
39
|
+
const fileConfig = await loadConfig(options.config);
|
|
40
|
+
const cliClients = options.clients
|
|
41
|
+
? options.clients.split(',').map((s) => s.trim())
|
|
42
|
+
: undefined;
|
|
43
|
+
const config = {
|
|
44
|
+
...fileConfig,
|
|
45
|
+
...(cliClients ? { clients: cliClients } : {}),
|
|
46
|
+
ignore: [
|
|
47
|
+
...(fileConfig.ignore ?? []),
|
|
48
|
+
...(options.ignore ? options.ignore.split(',').map((s) => s.trim()) : []),
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
let tools;
|
|
52
|
+
if (options.server === 'stdio') {
|
|
53
|
+
// Get everything after '--' in process.argv
|
|
54
|
+
const rawArgv = process.argv;
|
|
55
|
+
const sepIdx = rawArgv.indexOf('--');
|
|
56
|
+
if (sepIdx === -1 || sepIdx >= rawArgv.length - 1) {
|
|
57
|
+
throw new Error('--server stdio requires a command after --, e.g.: mcp-lint check --server stdio -- node server.js');
|
|
58
|
+
}
|
|
59
|
+
const [cmd, ...cmdArgs] = rawArgv.slice(sepIdx + 1);
|
|
60
|
+
tools = await loadStdio(cmd, cmdArgs);
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
else if (options.server === 'sse') {
|
|
63
|
+
if (!options.url)
|
|
64
|
+
throw new Error('--server sse requires --url <url>');
|
|
65
|
+
tools = await loadSSE(options.url);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (!input)
|
|
69
|
+
throw new Error('Specify an input file or use --server stdio|sse');
|
|
70
|
+
tools = await loadFile(input);
|
|
71
|
+
}
|
|
72
|
+
const pluginRules = await loadPluginRules(fileConfig);
|
|
73
|
+
const engine = new LintEngine([...allRules, ...pluginRules], config);
|
|
74
|
+
let diagnostics = engine.lint(tools);
|
|
75
|
+
const minSeverity = options.quiet
|
|
76
|
+
? 'error'
|
|
77
|
+
: options.severity ?? 'info';
|
|
78
|
+
diagnostics = filterBySeverity(diagnostics, minSeverity);
|
|
79
|
+
let output;
|
|
80
|
+
if (options.format === 'json') {
|
|
81
|
+
output = formatJson(diagnostics);
|
|
82
|
+
}
|
|
83
|
+
else if (options.format === 'markdown') {
|
|
84
|
+
output = formatMarkdown(diagnostics);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
output = formatTerminal(diagnostics, !options.color);
|
|
88
|
+
}
|
|
89
|
+
if (options.outputFile) {
|
|
90
|
+
await writeFile(options.outputFile, output, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
process.stdout.write(output);
|
|
94
|
+
}
|
|
95
|
+
if (options.score) {
|
|
96
|
+
const report = computeQualityReport(tools, diagnostics);
|
|
97
|
+
const gradeColor = (g) => g === 'A' ? chalk.green(g) : g === 'B' ? chalk.cyan(g) : g === 'C' ? chalk.yellow(g) : chalk.red(g);
|
|
98
|
+
process.stdout.write(chalk.bold('\nQuality Score\n'));
|
|
99
|
+
for (const t of report.tools) {
|
|
100
|
+
process.stdout.write(` ${t.toolName.padEnd(30)} ${String(t.score).padStart(3)}/100 ${gradeColor(t.grade)} (${t.errors} errors, ${t.warnings} warnings)\n`);
|
|
101
|
+
}
|
|
102
|
+
process.stdout.write(`\n Overall: ${chalk.bold(String(report.overall))}/100 ${gradeColor(report.grade)}\n\n`);
|
|
103
|
+
}
|
|
104
|
+
const hasErrors = diagnostics.some((d) => d.severity === 'error');
|
|
105
|
+
return hasErrors;
|
|
72
106
|
}
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
catch (err) {
|
|
108
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
109
|
+
return true;
|
|
75
110
|
}
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
}
|
|
112
|
+
if (options.watch) {
|
|
113
|
+
if (!input || options.server) {
|
|
114
|
+
process.stderr.write('Error: --watch only works with file input\n');
|
|
115
|
+
process.exit(2);
|
|
78
116
|
}
|
|
79
|
-
process.stdout.write(
|
|
80
|
-
|
|
81
|
-
|
|
117
|
+
process.stdout.write(chalk.dim(`Watching ${input} for changes...\n\n`));
|
|
118
|
+
await runLint();
|
|
119
|
+
let debounce = null;
|
|
120
|
+
watch(input, () => {
|
|
121
|
+
if (debounce)
|
|
122
|
+
clearTimeout(debounce);
|
|
123
|
+
debounce = setTimeout(async () => {
|
|
124
|
+
process.stdout.write(chalk.dim(`\n[${new Date().toLocaleTimeString()}] File changed — re-linting...\n\n`));
|
|
125
|
+
await runLint();
|
|
126
|
+
}, 100);
|
|
127
|
+
});
|
|
82
128
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
process.exit(
|
|
129
|
+
else {
|
|
130
|
+
const hasErrors = await runLint();
|
|
131
|
+
process.exit(hasErrors ? 1 : 0);
|
|
86
132
|
}
|
|
87
133
|
});
|
|
88
134
|
}
|