mcp-proof 0.3.6 → 0.3.7

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/bin/mcp-proof.js CHANGED
@@ -23,7 +23,7 @@ const fs = require('fs');
23
23
  const os = require('os');
24
24
  const path = require('path');
25
25
  const { spawn } = require('child_process');
26
- const { evaluate, scanSkillManifest, verifyPublisherAsync } = require('../engine.js');
26
+ const { evaluate, evaluateCall, scanSkillManifest, verifyPublisherAsync } = require('../engine.js');
27
27
 
28
28
  const DEFAULT_MAX_INPUT_BYTES = 5 * 1024 * 1024;
29
29
  const DEFAULT_TIMEOUT_MS = 15000;
@@ -43,13 +43,15 @@ Usage:
43
43
  mcp-proof --stdio "<command to launch MCP server>"
44
44
  mcp-proof --config <claude-or-cursor-config.json>
45
45
  mcp-proof --skill <SKILL.md>
46
+ mcp-proof --execute [<call.json>] runtime authorization of a tools/call payload
46
47
 
47
- If <file.json> is omitted and --stdio/--config/--skill are not used, JSON is read from stdin.
48
+ If <file.json> is omitted and --stdio/--config/--skill/--execute are not used, JSON is read from stdin.
48
49
 
49
50
  Options:
50
51
  --stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
51
52
  --config <path> scan every stdio server in an MCP client config with mcpServers
52
53
  --skill <path> scan a Claude SKILL.md (educational/warning context aware)
54
+ --execute [<path>] runtime authorization of a tools/call payload (reads stdin if path omitted)
53
55
  --live confirm npm publisher against registry.npmjs.org (one network call)
54
56
  --timeout <ms> MCP launch/protocol timeout per server (default: 15000)
55
57
  --source <s> npm:@scope/pkg, git+https://..., or file://... (drives publisher verification)
@@ -89,9 +91,9 @@ async function main() {
89
91
  if (args.selfTest) {
90
92
  return runSelfTest(args);
91
93
  }
92
- const exclusiveModes = [args.stdio, args.config, args.skill].filter(Boolean).length;
94
+ const exclusiveModes = [args.stdio, args.config, args.skill, args.execute].filter(Boolean).length;
93
95
  if (exclusiveModes > 1) {
94
- process.stderr.write('mcp-proof: use one of --stdio, --config, --skill at a time.\n');
96
+ process.stderr.write('mcp-proof: use one of --stdio, --config, --skill, --execute at a time.\n');
95
97
  process.exit(64);
96
98
  }
97
99
 
@@ -151,6 +153,22 @@ async function main() {
151
153
  process.exit(exitCodeForDecision(result.decision));
152
154
  }
153
155
 
156
+ if (args.execute) {
157
+ let result;
158
+ try {
159
+ result = runExecute(args);
160
+ } catch (err) {
161
+ process.stderr.write(`mcp-proof: ${err.message}\n`);
162
+ process.exit(err.exitCode || 65);
163
+ }
164
+ if (args.json) {
165
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
166
+ } else {
167
+ renderHumanReadable(result, 0, result.tool || 'execute');
168
+ }
169
+ process.exit(exitCodeForDecision(result.decision));
170
+ }
171
+
154
172
  let raw;
155
173
  try {
156
174
  raw = args.file ? readFileLimited(args.file) : readStdinLimited();
@@ -206,7 +224,7 @@ function parseArgs(argv) {
206
224
  const out = {
207
225
  source: '', transport: '', bind: '', json: false, selfTest: false,
208
226
  help: false, version: false, file: '', stdio: '', config: '', skill: '',
209
- live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
227
+ execute: false, live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
210
228
  };
211
229
  for (let i = 0; i < argv.length; i++) {
212
230
  const a = argv[i];
@@ -221,6 +239,10 @@ function parseArgs(argv) {
221
239
  case '--stdio': out.stdio = argv[++i] || ''; break;
222
240
  case '--config': out.config = expandHome(argv[++i] || ''); break;
223
241
  case '--skill': out.skill = expandHome(argv[++i] || ''); break;
242
+ case '--execute':
243
+ out.execute = true;
244
+ if (argv[i + 1] && !argv[i + 1].startsWith('-')) out.file = argv[++i];
245
+ break;
224
246
  case '--live': out.live = true; break;
225
247
  case '--timeout': out.timeoutMs = parseTimeout(argv[++i] || ''); break;
226
248
  default:
@@ -577,6 +599,25 @@ function worstDecision(decisions) {
577
599
  return 'allow';
578
600
  }
579
601
 
602
+ function runExecute(args) {
603
+ const raw = args.file ? readFileLimited(args.file) : readStdinLimited();
604
+ if (!raw.trim()) {
605
+ throw new Error('empty input. Pipe a tools/call JSON or pass a path.');
606
+ }
607
+ let parsed;
608
+ try {
609
+ parsed = JSON.parse(raw);
610
+ } catch (err) {
611
+ const e = new Error(`invalid JSON: ${err.message}`);
612
+ e.exitCode = 65;
613
+ throw e;
614
+ }
615
+ const result = evaluateCall(parsed);
616
+ result.mode = 'execute';
617
+ result.name = args.file || 'stdin';
618
+ return result;
619
+ }
620
+
580
621
  function exitCodeForDecision(d) {
581
622
  return d === 'allow' ? 0 : d === 'warn' ? 1 : 2;
582
623
  }
package/engine.js CHANGED
@@ -634,4 +634,67 @@ function evaluate(req) {
634
634
 
635
635
  function round1(v) { return Math.round(v * 10) / 10; }
636
636
 
637
- module.exports = { evaluate, scanTools, scanSkillManifest, verifyPublisher, verifyPublisherAsync, validateTransport, levenshtein };
637
+ // ── Runtime tool-call authorization ──────────────────────────────────────────
638
+ //
639
+ // evaluateCall answers the execution-time question: given the exact tool and
640
+ // the exact arguments, is this call still authorized? It applies the same
641
+ // signatures used for static tool manifest scanning to the live payload.
642
+
643
+ function flattenArgs(args) {
644
+ const values = [];
645
+ function walk(v) {
646
+ if (v == null) return;
647
+ if (typeof v === 'string') values.push(v);
648
+ else if (Array.isArray(v)) v.forEach(walk);
649
+ else if (typeof v === 'object') Object.values(v).forEach(walk);
650
+ else values.push(String(v));
651
+ }
652
+ walk(args);
653
+ return values.join('\n');
654
+ }
655
+
656
+ function scanCall(toolName, args) {
657
+ const hay = [toolName, flattenArgs(args)].join('\n');
658
+ for (const sig of SIGNATURES) {
659
+ if (sig.re.test(hay)) {
660
+ return { detected: true, severity: sig.sev, reason: sig.reason };
661
+ }
662
+ }
663
+ if (HIDDEN_CHARS.test(hay)) {
664
+ return { detected: true, severity: 'high', reason: 'arguments contain hidden unicode characters' };
665
+ }
666
+ const b64 = hay.match(BASE64_RE);
667
+ if (b64) {
668
+ for (const m of b64) {
669
+ try {
670
+ const decoded = Buffer.from(m, 'base64').toString('utf8');
671
+ if (SUSPICIOUS_DECODED.test(decoded)) {
672
+ return { detected: true, severity: 'high', reason: 'arguments contain base64 with suspicious decoded content' };
673
+ }
674
+ } catch (_) { /* ignore */ }
675
+ }
676
+ }
677
+ return { detected: false, severity: 'low', reason: 'clean' };
678
+ }
679
+
680
+ function evaluateCall(req) {
681
+ const start = Date.now();
682
+ const toolName = req.tool || (req.params && req.params.name) || '';
683
+ const args = req.arguments || (req.params && req.params.arguments) || {};
684
+ const scan = scanCall(toolName, args);
685
+ const score = {
686
+ tool: toolName,
687
+ call_scan: scan,
688
+ reasons: [],
689
+ decision: 'allow',
690
+ };
691
+ if (scan.detected) {
692
+ score.reasons.push(`${scan.severity}: ${scan.reason}`);
693
+ if (scan.severity === 'critical') score.decision = 'block';
694
+ else score.decision = 'warn';
695
+ }
696
+ score.durationMs = Date.now() - start;
697
+ return score;
698
+ }
699
+
700
+ module.exports = { evaluate, evaluateCall, scanTools, scanCall, scanSkillManifest, verifyPublisher, verifyPublisherAsync, validateTransport, levenshtein };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proof",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Unbranded, free MCP (Model Context Protocol) server verifier — detects tool poisoning, typosquats, hidden unicode, base64 payloads, exposed transports, and skill-manifest attacks. Same engine as the VyriAI control plane, no account required.",
5
5
  "main": "engine.js",
6
6
  "bin": {