mcp-lint 0.3.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 CHANGED
@@ -440,6 +440,116 @@ const fixed = applyFixes(tools, diagnostics);
440
440
 
441
441
  ---
442
442
 
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:
448
+
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
514
+
515
+ ```typescript
516
+ import { preflight, loadPolicy } from 'mcp-lint/preflight';
517
+
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',
526
+ },
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
+ }
539
+ ```
540
+
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.
550
+
551
+ ---
552
+
443
553
  ## License
444
554
 
445
555
  MIT © Robert
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerPreflightCommand(program: Command): void;
@@ -0,0 +1,109 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import chalk from 'chalk';
3
+ import { preflight } from '../../preflight/engine.js';
4
+ import { loadPolicy } from '../../preflight/policy.js';
5
+ export function registerPreflightCommand(program) {
6
+ program
7
+ .command('preflight [action-file]')
8
+ .description('Evaluate a proposed agent action against a policy before execution')
9
+ .option('--policy <path>', 'Path to preflight policy YAML file')
10
+ .option('--format <format>', 'Output format: terminal|json', 'terminal')
11
+ .option('--no-color', 'Disable terminal colors')
12
+ .option('--stdin', 'Read action JSON from stdin')
13
+ .action(async (actionFile, options) => {
14
+ if (!options.color)
15
+ chalk.level = 0;
16
+ try {
17
+ const action = await readAction(actionFile, options.stdin ?? false);
18
+ const policy = options.policy ? await loadPolicy(options.policy) : undefined;
19
+ const decision = preflight(action, policy);
20
+ if (options.format === 'json') {
21
+ process.stdout.write(JSON.stringify(decision, null, 2) + '\n');
22
+ }
23
+ else {
24
+ process.stdout.write(renderTerminal(decision));
25
+ }
26
+ const exitCode = decisionExitCode(decision);
27
+ process.exit(exitCode);
28
+ }
29
+ catch (err) {
30
+ process.stderr.write(`Error: ${err.message}\n`);
31
+ process.exit(2);
32
+ }
33
+ });
34
+ }
35
+ async function readAction(path, stdin) {
36
+ let raw;
37
+ if (stdin || !path) {
38
+ if (process.stdin.isTTY && !stdin) {
39
+ throw new Error('Specify an action file or use --stdin');
40
+ }
41
+ raw = await readStdin();
42
+ }
43
+ else {
44
+ raw = await readFile(path, 'utf8');
45
+ }
46
+ return JSON.parse(raw);
47
+ }
48
+ async function readStdin() {
49
+ const chunks = [];
50
+ for await (const chunk of process.stdin) {
51
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
52
+ }
53
+ return Buffer.concat(chunks).toString('utf8');
54
+ }
55
+ function decisionExitCode(d) {
56
+ switch (d.decision) {
57
+ case 'allow':
58
+ case 'allow_with_rewrite':
59
+ return 0;
60
+ case 'require_approval':
61
+ return 1;
62
+ case 'deny':
63
+ return 2;
64
+ }
65
+ }
66
+ function renderTerminal(d) {
67
+ const lines = [];
68
+ const decisionColor = d.decision === 'deny'
69
+ ? chalk.red
70
+ : d.decision === 'require_approval'
71
+ ? chalk.yellow
72
+ : d.decision === 'allow_with_rewrite'
73
+ ? chalk.cyan
74
+ : chalk.green;
75
+ lines.push('');
76
+ lines.push(chalk.bold('Preflight Decision'));
77
+ lines.push('─'.repeat(40));
78
+ lines.push(`Decision: ${decisionColor(d.decision.toUpperCase())}`);
79
+ lines.push(`Risk score: ${riskColor(d.risk_score)}`);
80
+ lines.push(`Reversibility: ${d.reversibility}`);
81
+ lines.push(`Est. cost: $${d.estimated_cost_usd.toFixed(2)}`);
82
+ if (d.matched_policies.length > 0) {
83
+ lines.push(`Policies: ${d.matched_policies.join(', ')}`);
84
+ }
85
+ if (d.reasons.length > 0) {
86
+ lines.push(`Reasons: ${d.reasons.join(', ')}`);
87
+ }
88
+ if (d.safe_alternative) {
89
+ lines.push('');
90
+ lines.push(chalk.bold('Safe alternative:'));
91
+ lines.push(` ${d.safe_alternative.tool_type} → ${d.safe_alternative.action}`);
92
+ if (d.safe_alternative.target) {
93
+ lines.push(` target: ${d.safe_alternative.target}`);
94
+ }
95
+ }
96
+ lines.push('');
97
+ return lines.join('\n');
98
+ }
99
+ function riskColor(score) {
100
+ const s = score.toFixed(2);
101
+ if (score >= 0.9)
102
+ return chalk.red(s);
103
+ if (score >= 0.7)
104
+ return chalk.yellow(s);
105
+ if (score >= 0.4)
106
+ return chalk.cyan(s);
107
+ return chalk.green(s);
108
+ }
109
+ //# sourceMappingURL=preflight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight.js","sourceRoot":"","sources":["../../../src/cli/commands/preflight.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -6,16 +6,18 @@ import { registerInitCommand } from './cli/commands/init.js';
6
6
  import { registerCompatCommand } from './cli/commands/compat.js';
7
7
  import { registerExplainCommand } from './cli/commands/explain.js';
8
8
  import { registerDiffCommand } from './cli/commands/diff.js';
9
+ import { registerPreflightCommand } from './cli/commands/preflight.js';
9
10
  const program = new Command();
10
11
  program
11
12
  .name('mcp-lint')
12
13
  .description('Lint MCP server tool schemas for cross-client compatibility')
13
- .version('0.3.0');
14
+ .version('0.4.0');
14
15
  registerCheckCommand(program);
15
16
  registerFixCommand(program);
16
17
  registerInitCommand(program);
17
18
  registerCompatCommand(program);
18
19
  registerExplainCommand(program);
19
20
  registerDiffCommand(program);
21
+ registerPreflightCommand(program);
20
22
  program.parse();
21
23
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC9B,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAC5B,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAE7B,OAAO,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAEvE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAC9B,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAC5B,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,wBAAwB,CAAC,OAAO,CAAC,CAAC;AAElC,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { Action, Decision, Policy } from './types.js';
2
+ export declare function preflight(action: Action, policy?: Policy): Decision;
@@ -0,0 +1,73 @@
1
+ import { matchRule } from './policy.js';
2
+ import { scoreAction } from './scorer.js';
3
+ const DEFAULT_APPROVAL_THRESHOLD = 0.7;
4
+ const DEFAULT_BLOCK_THRESHOLD = 0.92;
5
+ export function preflight(action, policy) {
6
+ validateAction(action);
7
+ const { score, reasons, reversibility } = scoreAction(action);
8
+ const approvalThreshold = policy?.defaults?.approval_threshold ?? DEFAULT_APPROVAL_THRESHOLD;
9
+ const blockThreshold = policy?.defaults?.block_threshold ?? DEFAULT_BLOCK_THRESHOLD;
10
+ const matched = [];
11
+ if (policy) {
12
+ for (const rule of policy.rules) {
13
+ if (matchRule(rule, action, score)) {
14
+ matched.push(rule);
15
+ }
16
+ }
17
+ }
18
+ let decision = null;
19
+ let safeAlternative = undefined;
20
+ const policyReasons = [];
21
+ for (const rule of matched) {
22
+ if (rule.reason)
23
+ policyReasons.push(rule.reason);
24
+ if (rule.effect === 'deny') {
25
+ decision = 'deny';
26
+ break;
27
+ }
28
+ if (rule.effect === 'require_approval') {
29
+ decision = 'require_approval';
30
+ }
31
+ else if (rule.effect === 'allow_with_rewrite' && decision === null) {
32
+ decision = 'allow_with_rewrite';
33
+ safeAlternative = rule.rewrite;
34
+ }
35
+ else if (rule.effect === 'allow' && decision === null) {
36
+ decision = 'allow';
37
+ }
38
+ }
39
+ if (decision === null) {
40
+ if (score >= blockThreshold) {
41
+ decision = 'deny';
42
+ policyReasons.push('risk_above_block_threshold');
43
+ }
44
+ else if (score >= approvalThreshold) {
45
+ decision = 'require_approval';
46
+ policyReasons.push('risk_above_approval_threshold');
47
+ }
48
+ else {
49
+ decision = 'allow';
50
+ }
51
+ }
52
+ return {
53
+ decision,
54
+ risk_score: Number(score.toFixed(2)),
55
+ reversibility,
56
+ estimated_cost_usd: action.estimated_cost_usd ?? 0,
57
+ matched_policies: matched.map((r) => r.id),
58
+ reasons: [...reasons, ...policyReasons],
59
+ safe_alternative: safeAlternative,
60
+ };
61
+ }
62
+ function validateAction(action) {
63
+ if (!action || typeof action !== 'object') {
64
+ throw new Error('Action must be an object');
65
+ }
66
+ if (!action.tool_type)
67
+ throw new Error('Action missing tool_type');
68
+ if (!action.tool_name)
69
+ throw new Error('Action missing tool_name');
70
+ if (typeof action.action !== 'string')
71
+ throw new Error('Action missing action string');
72
+ }
73
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/preflight/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { preflight } from './engine.js';
2
+ export { loadPolicy, parsePolicy, matchRule } from './policy.js';
3
+ export { scoreAction } from './scorer.js';
4
+ export type { Action, Decision, DecisionType, Effect, Policy, PolicyRule, Rewrite, Reversibility, ToolType, } from './types.js';
@@ -0,0 +1,4 @@
1
+ export { preflight } from './engine.js';
2
+ export { loadPolicy, parsePolicy, matchRule } from './policy.js';
3
+ export { scoreAction } from './scorer.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/preflight/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Action, Policy, PolicyRule } from './types.js';
2
+ export declare function loadPolicy(path: string): Promise<Policy>;
3
+ export declare function parsePolicy(yaml: string): Policy;
4
+ export declare function matchRule(rule: PolicyRule, action: Action, riskScore: number): boolean;
@@ -0,0 +1,97 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import YAML from 'yaml';
3
+ export async function loadPolicy(path) {
4
+ const raw = await readFile(path, 'utf8');
5
+ const parsed = YAML.parse(raw);
6
+ return normalizePolicy(parsed);
7
+ }
8
+ export function parsePolicy(yaml) {
9
+ const parsed = YAML.parse(yaml);
10
+ return normalizePolicy(parsed);
11
+ }
12
+ function normalizePolicy(parsed) {
13
+ if (!parsed || typeof parsed !== 'object') {
14
+ throw new Error('Policy file is empty or not an object');
15
+ }
16
+ const rules = Array.isArray(parsed.rules) ? parsed.rules : [];
17
+ for (const [idx, rule] of rules.entries()) {
18
+ if (!rule.id)
19
+ throw new Error(`Policy rule #${idx} missing "id"`);
20
+ if (!rule.when)
21
+ throw new Error(`Policy rule "${rule.id}" missing "when"`);
22
+ if (!rule.effect)
23
+ throw new Error(`Policy rule "${rule.id}" missing "effect"`);
24
+ }
25
+ return {
26
+ version: parsed.version ?? 1,
27
+ defaults: parsed.defaults ?? {},
28
+ budgets: parsed.budgets ?? {},
29
+ rules,
30
+ };
31
+ }
32
+ export function matchRule(rule, action, riskScore) {
33
+ const w = rule.when;
34
+ if (w.tool_type) {
35
+ const types = Array.isArray(w.tool_type) ? w.tool_type : [w.tool_type];
36
+ if (!types.includes(action.tool_type))
37
+ return false;
38
+ }
39
+ if (w.tool_name) {
40
+ const names = Array.isArray(w.tool_name) ? w.tool_name : [w.tool_name];
41
+ if (!names.includes(action.tool_name))
42
+ return false;
43
+ }
44
+ if (w.action_matches && w.action_matches.length > 0) {
45
+ const hit = w.action_matches.some((m) => action.action.includes(m));
46
+ if (!hit)
47
+ return false;
48
+ }
49
+ if (w.target_matches && w.target_matches.length > 0) {
50
+ const target = action.target ?? '';
51
+ const hit = w.target_matches.some((m) => matchesGlob(target, m));
52
+ if (!hit)
53
+ return false;
54
+ }
55
+ if (w.domain_not_in_allowlist) {
56
+ const domain = extractDomain(action);
57
+ const allow = w.allowlist ?? [];
58
+ if (!domain)
59
+ return false;
60
+ if (allow.includes(domain))
61
+ return false;
62
+ }
63
+ if (typeof w.min_risk === 'number') {
64
+ if (riskScore < w.min_risk)
65
+ return false;
66
+ }
67
+ if (typeof w.max_cost_usd === 'number') {
68
+ const cost = action.estimated_cost_usd ?? 0;
69
+ if (cost <= w.max_cost_usd)
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+ function matchesGlob(value, pattern) {
75
+ if (pattern.includes('**') || pattern.includes('*')) {
76
+ const regex = new RegExp('^' +
77
+ pattern
78
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
79
+ .replace(/\*\*/g, '§§')
80
+ .replace(/\*/g, '[^/]*')
81
+ .replace(/§§/g, '.*') +
82
+ '$');
83
+ return regex.test(value);
84
+ }
85
+ return value.includes(pattern);
86
+ }
87
+ function extractDomain(action) {
88
+ const candidate = action.target ?? action.action;
89
+ const match = candidate.match(/https?:\/\/([^/\s]+)/i);
90
+ if (match)
91
+ return match[1].toLowerCase();
92
+ if (action.context && typeof action.context.domain === 'string') {
93
+ return action.context.domain.toLowerCase();
94
+ }
95
+ return null;
96
+ }
97
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/preflight/policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Action, Reversibility } from './types.js';
2
+ export interface ScoreBreakdown {
3
+ score: number;
4
+ reasons: string[];
5
+ reversibility: Reversibility;
6
+ }
7
+ export declare function scoreAction(action: Action): ScoreBreakdown;
@@ -0,0 +1,121 @@
1
+ const BASE_SCORES = {
2
+ file_read: 0.1,
3
+ file_write: 0.35,
4
+ file_delete: 0.75,
5
+ shell: 0.5,
6
+ http: 0.35,
7
+ mcp_tool: 0.3,
8
+ email: 0.6,
9
+ payment: 0.95,
10
+ database: 0.7,
11
+ other: 0.4,
12
+ };
13
+ const DESTRUCTIVE_PATTERNS = [
14
+ /\brm\s+-rf\b/i,
15
+ /\brmdir\s+\/s\b/i,
16
+ /\bdel\s+\/s\b/i,
17
+ /\bdrop\s+table\b/i,
18
+ /\btruncate\s+table\b/i,
19
+ /\bformat\s+\/\w+/i,
20
+ /\bmkfs\b/i,
21
+ /\b:\(\)\{\s*:\|:&\s*\};:/,
22
+ /\bgit\s+push\s+.*--force\b/i,
23
+ /\bgit\s+reset\s+--hard\b/i,
24
+ ];
25
+ const SENSITIVE_TARGETS = [
26
+ /\/etc\b/,
27
+ /\/prod\b/,
28
+ /\/production\b/,
29
+ /\/app\/data\b/,
30
+ /\.env(\.|$)/,
31
+ /\bsecrets?\b/i,
32
+ /\bcredentials?\b/i,
33
+ /\/root\b/,
34
+ /\bC:\\Windows\b/i,
35
+ ];
36
+ const MASS_OPERATION_PATTERNS = [
37
+ /\*\*\/\*/,
38
+ /\brecursive\b/i,
39
+ /\s+-r\b/,
40
+ /\s+-R\b/,
41
+ /\bALL\b/,
42
+ ];
43
+ export function scoreAction(action) {
44
+ const reasons = [];
45
+ let score = BASE_SCORES[action.tool_type] ?? 0.4;
46
+ reasons.push(`base_${action.tool_type}`);
47
+ const actionText = action.action ?? '';
48
+ const targetText = action.target ?? '';
49
+ const haystack = `${actionText} ${targetText}`;
50
+ let destructive = false;
51
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
52
+ if (pattern.test(haystack)) {
53
+ destructive = true;
54
+ score += 0.25;
55
+ reasons.push('destructive_pattern');
56
+ break;
57
+ }
58
+ }
59
+ if (action.tool_type === 'file_delete') {
60
+ destructive = true;
61
+ }
62
+ for (const pattern of SENSITIVE_TARGETS) {
63
+ if (pattern.test(haystack)) {
64
+ score += 0.2;
65
+ reasons.push('sensitive_target');
66
+ break;
67
+ }
68
+ }
69
+ for (const pattern of MASS_OPERATION_PATTERNS) {
70
+ if (pattern.test(haystack)) {
71
+ score += 0.1;
72
+ reasons.push('mass_operation');
73
+ break;
74
+ }
75
+ }
76
+ if (action.context?.environment === 'prod' || action.context?.environment === 'production') {
77
+ score += 0.15;
78
+ reasons.push('prod_environment');
79
+ }
80
+ const cost = action.estimated_cost_usd ?? 0;
81
+ if (cost >= 10) {
82
+ score += 0.15;
83
+ reasons.push('high_cost');
84
+ }
85
+ else if (cost >= 1) {
86
+ score += 0.05;
87
+ reasons.push('nontrivial_cost');
88
+ }
89
+ score = Math.min(1, Math.max(0, score));
90
+ const reversibility = computeReversibility(action, destructive);
91
+ if (reversibility === 'irreversible') {
92
+ reasons.push('irreversible_operation');
93
+ }
94
+ return { score, reasons, reversibility };
95
+ }
96
+ function computeReversibility(action, destructive) {
97
+ if (destructive)
98
+ return 'irreversible';
99
+ switch (action.tool_type) {
100
+ case 'file_read':
101
+ return 'reversible';
102
+ case 'file_write':
103
+ return 'partially_reversible';
104
+ case 'file_delete':
105
+ return 'irreversible';
106
+ case 'payment':
107
+ case 'email':
108
+ return 'irreversible';
109
+ case 'http':
110
+ return 'partially_reversible';
111
+ case 'database':
112
+ return 'partially_reversible';
113
+ case 'shell':
114
+ return 'partially_reversible';
115
+ case 'mcp_tool':
116
+ return 'partially_reversible';
117
+ default:
118
+ return 'partially_reversible';
119
+ }
120
+ }
121
+ //# sourceMappingURL=scorer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scorer.js","sourceRoot":"","sources":["../../src/preflight/scorer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ export type ToolType = 'shell' | 'file_read' | 'file_write' | 'file_delete' | 'http' | 'mcp_tool' | 'email' | 'payment' | 'database' | 'other';
2
+ export type Reversibility = 'reversible' | 'partially_reversible' | 'irreversible';
3
+ export type DecisionType = 'allow' | 'deny' | 'require_approval' | 'allow_with_rewrite';
4
+ export type Effect = 'allow' | 'deny' | 'require_approval' | 'allow_with_rewrite';
5
+ export interface Action {
6
+ actor_id?: string;
7
+ workspace_id?: string;
8
+ tool_type: ToolType;
9
+ tool_name: string;
10
+ action: string;
11
+ target?: string;
12
+ context?: Record<string, unknown>;
13
+ estimated_cost_usd?: number;
14
+ }
15
+ export interface Rewrite {
16
+ tool_type: ToolType;
17
+ tool_name?: string;
18
+ action: string;
19
+ target?: string;
20
+ }
21
+ export interface PolicyRule {
22
+ id: string;
23
+ when: {
24
+ tool_type?: ToolType | ToolType[];
25
+ tool_name?: string | string[];
26
+ action_matches?: string[];
27
+ target_matches?: string[];
28
+ domain_not_in_allowlist?: boolean;
29
+ allowlist?: string[];
30
+ min_risk?: number;
31
+ max_cost_usd?: number;
32
+ };
33
+ effect: Effect;
34
+ rewrite?: Rewrite;
35
+ reason?: string;
36
+ }
37
+ export interface Policy {
38
+ version: number;
39
+ defaults?: {
40
+ approval_threshold?: number;
41
+ block_threshold?: number;
42
+ };
43
+ budgets?: {
44
+ max_http_cost_usd_per_action?: number;
45
+ max_model_cost_usd_per_action?: number;
46
+ };
47
+ rules: PolicyRule[];
48
+ }
49
+ export interface Decision {
50
+ decision: DecisionType;
51
+ risk_score: number;
52
+ reversibility: Reversibility;
53
+ estimated_cost_usd: number;
54
+ matched_policies: string[];
55
+ reasons: string[];
56
+ safe_alternative?: Rewrite;
57
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/preflight/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ {
2
+ "actor_id": "agent:claude-code",
3
+ "workspace_id": "team-alpha",
4
+ "tool_type": "shell",
5
+ "tool_name": "bash",
6
+ "action": "rm -rf /prod/data",
7
+ "target": "/prod/data",
8
+ "context": {
9
+ "environment": "prod"
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "actor_id": "agent:claude-code",
3
+ "workspace_id": "team-alpha",
4
+ "tool_type": "shell",
5
+ "tool_name": "bash",
6
+ "action": "rm -rf ./tmp",
7
+ "target": "./tmp"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "actor_id": "agent:claude-code",
3
+ "workspace_id": "team-alpha",
4
+ "tool_type": "file_read",
5
+ "tool_name": "read",
6
+ "action": "cat package.json",
7
+ "target": "./package.json"
8
+ }
@@ -0,0 +1,23 @@
1
+ name: MCP Schema Lint (example)
2
+
3
+ # Example workflow showing how to use mcp-lint in CI.
4
+ # Copy this to your own repo and adjust the `input` path.
5
+
6
+ on:
7
+ pull_request:
8
+ push:
9
+ branches: [main]
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Lint MCP tool schemas
18
+ uses: robert19001-cmyk/mcp-lint@v0.3.0
19
+ with:
20
+ input: tools.json
21
+ severity: warning
22
+ score: 'true'
23
+ fail_on: error
@@ -0,0 +1,59 @@
1
+ version: 1
2
+
3
+ defaults:
4
+ approval_threshold: 0.70
5
+ block_threshold: 0.92
6
+
7
+ budgets:
8
+ max_http_cost_usd_per_action: 2
9
+ max_model_cost_usd_per_action: 1
10
+
11
+ rules:
12
+ - id: block-destructive-prod
13
+ when:
14
+ tool_type: shell
15
+ action_matches: ["rm -rf", "rmdir /s", "del /s"]
16
+ target_matches: ["/prod", "/etc", "/app/data", "C:\\Windows"]
17
+ effect: deny
18
+ reason: Destructive operations on production paths are forbidden
19
+
20
+ - id: approval-sensitive-write
21
+ when:
22
+ tool_type: file_write
23
+ target_matches: ["**/.env*", "**/secrets*", "**/credentials*"]
24
+ effect: require_approval
25
+ reason: Writing to sensitive files requires approval
26
+
27
+ - id: rewrite-tmp-delete
28
+ when:
29
+ tool_type: shell
30
+ action_matches: ["rm -rf ./tmp", "rm -rf ./cache", "rm -rf ./build"]
31
+ effect: allow_with_rewrite
32
+ rewrite:
33
+ tool_type: file_delete
34
+ action: move_to_trash
35
+ target: ./tmp
36
+ reason: Prefer moving to trash over hard delete
37
+
38
+ - id: allow-workspace-writes
39
+ when:
40
+ tool_type: file_write
41
+ target_matches: ["./workspace/**", "./src/**", "./tests/**"]
42
+ effect: allow
43
+
44
+ - id: allow-local-reads
45
+ when:
46
+ tool_type: file_read
47
+ effect: allow
48
+
49
+ - id: approval-payments
50
+ when:
51
+ tool_type: payment
52
+ effect: require_approval
53
+ reason: All payments require human approval
54
+
55
+ - id: approval-external-email
56
+ when:
57
+ tool_type: email
58
+ effect: require_approval
59
+ reason: Outgoing email requires approval
@@ -0,0 +1,33 @@
1
+ version: 1
2
+
3
+ defaults:
4
+ approval_threshold: 0.85
5
+ block_threshold: 0.98
6
+
7
+ rules:
8
+ - id: block-prod-paths
9
+ when:
10
+ target_matches: ["/prod/**", "/production/**"]
11
+ effect: deny
12
+ reason: Never touch production paths from dev
13
+
14
+ - id: allow-all-reads
15
+ when:
16
+ tool_type: file_read
17
+ effect: allow
18
+
19
+ - id: allow-workspace
20
+ when:
21
+ tool_type: file_write
22
+ target_matches: ["./**"]
23
+ effect: allow
24
+
25
+ - id: rewrite-destructive-delete
26
+ when:
27
+ tool_type: shell
28
+ action_matches: ["rm -rf"]
29
+ effect: allow_with_rewrite
30
+ rewrite:
31
+ tool_type: file_delete
32
+ action: move_to_trash
33
+ reason: Use trash instead of hard delete even in dev
@@ -0,0 +1,42 @@
1
+ version: 1
2
+
3
+ defaults:
4
+ approval_threshold: 0.40
5
+ block_threshold: 0.80
6
+
7
+ rules:
8
+ - id: approval-all-shell
9
+ when:
10
+ tool_type: shell
11
+ effect: require_approval
12
+ reason: All shell commands require approval under strict policy
13
+
14
+ - id: approval-all-http
15
+ when:
16
+ tool_type: http
17
+ domain_not_in_allowlist: true
18
+ allowlist:
19
+ - api.github.com
20
+ - api.anthropic.com
21
+ - api.openai.com
22
+ effect: require_approval
23
+ reason: HTTP calls outside the allowlist require approval
24
+
25
+ - id: block-destructive
26
+ when:
27
+ tool_type: shell
28
+ action_matches: ["rm -rf", "drop table", "truncate"]
29
+ effect: deny
30
+ reason: Destructive commands are never allowed
31
+
32
+ - id: block-secrets
33
+ when:
34
+ action_matches: [".env", "secrets", "credentials", "id_rsa"]
35
+ effect: deny
36
+ reason: Secrets must never be touched by an agent
37
+
38
+ - id: approval-payment
39
+ when:
40
+ tool_type: payment
41
+ effect: deny
42
+ reason: Agents cannot make payments in strict mode
package/package.json CHANGED
@@ -1,22 +1,41 @@
1
1
  {
2
2
  "name": "mcp-lint",
3
- "version": "0.3.0",
4
- "description": "Lint MCP server tool schemas for cross-client compatibility",
3
+ "version": "0.4.0",
4
+ "description": "Lint MCP server tool schemas for cross-client compatibility + runtime preflight for agent tool calls",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mcp-lint": "./dist/index.js"
8
8
  },
9
9
  "main": "./dist/core/engine.js",
10
10
  "types": "./dist/core/engine.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/core/engine.d.ts",
14
+ "default": "./dist/core/engine.js"
15
+ },
16
+ "./preflight": {
17
+ "types": "./dist/preflight/index.d.ts",
18
+ "default": "./dist/preflight/index.js"
19
+ },
20
+ "./rules": {
21
+ "types": "./dist/rules/index.d.ts",
22
+ "default": "./dist/rules/index.js"
23
+ }
24
+ },
11
25
  "files": [
12
- "dist/"
26
+ "dist/",
27
+ "examples/"
13
28
  ],
14
29
  "keywords": [
15
30
  "mcp",
16
31
  "model-context-protocol",
17
32
  "lint",
18
33
  "schema",
19
- "validation"
34
+ "validation",
35
+ "preflight",
36
+ "agent-safety",
37
+ "policy",
38
+ "guardrails"
20
39
  ],
21
40
  "license": "MIT",
22
41
  "engines": {