mcp-proof 0.3.10 → 0.3.12

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
@@ -29,7 +29,7 @@ npx mcp-proof --config ~/.config/Claude/claude_desktop_config.json
29
29
  npx mcp-proof --skill ~/.claude/skills/example/SKILL.md
30
30
 
31
31
  # Inspect an exact tools/call payload before execution
32
- cat call.json | npx mcp-proof --execute --json
32
+ cat call.json | npx mcp-proof --inspect-call --json
33
33
 
34
34
  # Create a shareable Trust Card for a PR or README
35
35
  npx mcp-proof tools.json --markdown > MCP_TRUST.md
@@ -134,7 +134,8 @@ Options:
134
134
  --stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
135
135
  --config <path> scan every stdio server in an MCP client config with mcpServers
136
136
  --skill <path> scan a Claude SKILL.md (educational/warning context aware)
137
- --execute [<path>] inspect a tools/call payload before execution
137
+ --inspect-call [<path>] inspect a tools/call payload before execution
138
+ --execute [<path>] deprecated alias for --inspect-call
138
139
  --markdown emit a shareable Markdown Trust Card
139
140
  --trust-card alias for --markdown
140
141
  --sarif emit SARIF 2.1.0 for GitHub code scanning
@@ -226,7 +227,7 @@ The npm CLI is local-first: MCP manifests, tool descriptions, tool arguments, cl
226
227
  - **Publisher verification is lexical only by default.** npm scope match + Levenshtein typosquat. Pass `--live` to confirm npm packages against `registry.npmjs.org` with one network call. *No GitHub-API lookup, no signature verification.*
227
228
  - **No source-tree scan.** The full VyriAI gateway also runs a static scanner over the package source for `child_process`, postinstall scripts, obfuscation. That step needs the source unpacked locally and isn't shipped in this CLI.
228
229
  - **No dependency CVE check.** The full gateway wraps `npm audit`; this CLI doesn't shell out.
229
- - **Limited runtime-payload inspection.** `--execute` scans a supplied `tools/call` name and arguments before execution; it does not execute the call, enforce an authorization policy, verify identity, or observe server runtime behavior.
230
+ - **Limited runtime-payload inspection.** `--inspect-call` scans a supplied `tools/call` name and arguments before execution (`--execute` remains a deprecated alias); it does not execute the call, enforce an authorization policy, verify identity, or observe server runtime behavior.
230
231
  - **Stdio discovery only.** `--stdio` / `--config` launches local stdio MCP servers and calls `tools/list`. It does not yet connect to HTTP/SSE MCP servers.
231
232
  - **Skill context is keyword-based.** The skill detector looks for warning/example/demonstration framing around sensitive signals. A well-disguised attack without that framing may not downgrade.
232
233
  - **Local payload cap.** The CLI refuses inputs over 5 MB by default to avoid accidental memory blowups in CI. Override with `MCP_SCAN_MAX_BYTES=20mb` if you have an unusually large tools manifest.
package/bin/mcp-proof.js CHANGED
@@ -9,9 +9,9 @@
9
9
  // mcp-proof --self-test
10
10
  //
11
11
  // Exit codes:
12
- // 0 — verdict allow
12
+ // 0 — verdict allow (and, with --vetokernel-endpoint, VetoKernel authorized it)
13
13
  // 1 — verdict warn
14
- // 2 — verdict block
14
+ // 2 — verdict block, or VetoKernel denied the action
15
15
  // 64 — usage error
16
16
  // 65 — input parse error
17
17
  //
@@ -24,6 +24,7 @@ const os = require('os');
24
24
  const path = require('path');
25
25
  const { spawn } = require('child_process');
26
26
  const { evaluate, evaluateCall, scanSkillManifest, verifyPublisherAsync } = require('../engine.js');
27
+ const { authorizeCall } = require('../vetokernel-client.js');
27
28
 
28
29
  const DEFAULT_MAX_INPUT_BYTES = 5 * 1024 * 1024;
29
30
  const DEFAULT_TIMEOUT_MS = 15000;
@@ -43,15 +44,19 @@ Usage:
43
44
  mcp-proof --stdio "<command to launch MCP server>"
44
45
  mcp-proof --config <claude-or-cursor-config.json>
45
46
  mcp-proof --skill <SKILL.md>
46
- mcp-proof --execute [<call.json>] runtime authorization of a tools/call payload
47
+ mcp-proof --inspect-call [<call.json>] inspect a tools/call payload before execution
47
48
 
48
- If <file.json> is omitted and --stdio/--config/--skill/--execute are not used, JSON is read from stdin.
49
+ If <file.json> is omitted and --stdio/--config/--skill/--inspect-call are not used, JSON is read from stdin.
49
50
 
50
51
  Options:
51
52
  --stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
52
53
  --config <path> scan every stdio server in an MCP client config with mcpServers
53
54
  --skill <path> scan a Claude SKILL.md (educational/warning context aware)
54
- --execute [<path>] inspect a tools/call payload before execution (reads stdin if path omitted)
55
+ --inspect-call [<path>] inspect a tools/call payload before execution (reads stdin if path omitted)
56
+ --execute [<path>] deprecated alias for --inspect-call
57
+ --vetokernel-endpoint <url> with --inspect-call, ask VetoKernel to authorize the
58
+ proposed action before execution and verify the returned receipt
59
+ (env: VETOKERNEL_ENDPOINT, VETOKERNEL_API_KEY, VETOKERNEL_TENANT_ID)
55
60
  --markdown emit a shareable Markdown Trust Card
56
61
  --trust-card alias for --markdown
57
62
  --sarif emit SARIF 2.1.0 for GitHub code scanning
@@ -94,9 +99,12 @@ async function main() {
94
99
  if (args.selfTest) {
95
100
  return runSelfTest(args);
96
101
  }
97
- const exclusiveModes = [args.stdio, args.config, args.skill, args.execute].filter(Boolean).length;
102
+ if (args.deprecatedExecute) {
103
+ process.stderr.write('mcp-proof: --execute is deprecated; use --inspect-call instead.\n');
104
+ }
105
+ const exclusiveModes = [args.stdio, args.config, args.skill, args.inspectCall].filter(Boolean).length;
98
106
  if (exclusiveModes > 1) {
99
- process.stderr.write('mcp-proof: use one of --stdio, --config, --skill, --execute at a time.\n');
107
+ process.stderr.write('mcp-proof: use one of --stdio, --config, --skill, --inspect-call at a time.\n');
100
108
  process.exit(64);
101
109
  }
102
110
 
@@ -144,16 +152,27 @@ async function main() {
144
152
  process.exit(exitCodeForDecision(result.decision));
145
153
  }
146
154
 
147
- if (args.execute) {
155
+ if (args.inspectCall) {
148
156
  let result;
157
+ let call;
149
158
  try {
150
- result = runExecute(args);
159
+ ({ result, call } = runExecute(args));
151
160
  } catch (err) {
152
161
  process.stderr.write(`mcp-proof: ${err.message}\n`);
153
162
  process.exit(err.exitCode || 65);
154
163
  }
164
+ if (args.vetokernelEndpoint) {
165
+ try {
166
+ result.authorization = await authorizeInspectedCall(result, call, args);
167
+ } catch (err) {
168
+ // Fail closed: if the authorization service is unreachable, the action
169
+ // is not authorized.
170
+ process.stderr.write(`mcp-proof: vetokernel authorization failed: ${err.message}\n`);
171
+ process.exit(2);
172
+ }
173
+ }
155
174
  emitOutput(result, args);
156
- process.exit(exitCodeForDecision(result.decision));
175
+ process.exit(exitCodeForInspectedCall(result));
157
176
  }
158
177
 
159
178
  let raw;
@@ -207,7 +226,8 @@ function parseArgs(argv) {
207
226
  const out = {
208
227
  source: '', transport: '', bind: '', json: false, selfTest: false,
209
228
  help: false, version: false, file: '', stdio: '', config: '', skill: '',
210
- execute: false, format: 'human', live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
229
+ inspectCall: false, deprecatedExecute: false, format: 'human', live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
230
+ vetokernelEndpoint: process.env.VETOKERNEL_ENDPOINT || '',
211
231
  };
212
232
  for (let i = 0; i < argv.length; i++) {
213
233
  const a = argv[i];
@@ -224,12 +244,18 @@ function parseArgs(argv) {
224
244
  case '--stdio': out.stdio = argv[++i] || ''; break;
225
245
  case '--config': out.config = expandHome(argv[++i] || ''); break;
226
246
  case '--skill': out.skill = expandHome(argv[++i] || ''); break;
247
+ case '--inspect-call':
248
+ out.inspectCall = true;
249
+ if (argv[i + 1] && !argv[i + 1].startsWith('-')) out.file = argv[++i];
250
+ break;
227
251
  case '--execute':
228
- out.execute = true;
252
+ out.inspectCall = true;
253
+ out.deprecatedExecute = true;
229
254
  if (argv[i + 1] && !argv[i + 1].startsWith('-')) out.file = argv[++i];
230
255
  break;
231
256
  case '--live': out.live = true; break;
232
257
  case '--timeout': out.timeoutMs = parseTimeout(argv[++i] || ''); break;
258
+ case '--vetokernel-endpoint': out.vetokernelEndpoint = argv[++i] || ''; break;
233
259
  default:
234
260
  if (a.startsWith('-')) {
235
261
  process.stderr.write(`mcp-proof: unknown flag ${a}\n`);
@@ -643,9 +669,42 @@ function runExecute(args) {
643
669
  throw e;
644
670
  }
645
671
  const result = evaluateCall(parsed);
646
- result.mode = 'execute';
672
+ result.mode = 'inspect-call';
647
673
  result.name = args.file || 'stdin';
648
- return result;
674
+ return { result, call: parsed };
675
+ }
676
+
677
+ // authorizeInspectedCall asks VetoKernel to authorize the exact action this
678
+ // call proposes. action_type and target_id come from the call file itself,
679
+ // falling back to the tool name and arguments.target_id, so a call fixture is
680
+ // self-describing and needs no extra flags.
681
+ function authorizeInspectedCall(result, call, args) {
682
+ const callArgs = call.arguments || (call.params && call.params.arguments) || {};
683
+ const parametersJSON = JSON.stringify(callArgs);
684
+ return authorizeCall({
685
+ endpoint: args.vetokernelEndpoint,
686
+ apiKey: process.env.VETOKERNEL_API_KEY || '',
687
+ tenantId: process.env.VETOKERNEL_TENANT_ID || 'default',
688
+ agentId: call.agent_id || 'mcp-proof',
689
+ actionId: call.action_id || `mcp-proof-${Date.now()}`,
690
+ toolName: result.tool,
691
+ actionType: call.action_type || result.tool,
692
+ targetId: call.target_id || callArgs.target_id || '',
693
+ parametersJSON,
694
+ context: call.context,
695
+ scanner: {
696
+ name: 'mcp-proof',
697
+ decision: result.decision,
698
+ severity: result.call_scan.severity,
699
+ reason: result.call_scan.reason,
700
+ },
701
+ timeoutMs: args.timeoutMs,
702
+ });
703
+ }
704
+
705
+ function exitCodeForInspectedCall(result) {
706
+ if (result.authorization && result.authorization.decision !== 'allow') return 2;
707
+ return exitCodeForDecision(result.decision);
649
708
  }
650
709
 
651
710
  function exitCodeForDecision(d) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proof",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "description": "Auditable, privacy-preserving MCP (Model Context Protocol) server verifier with local Trust Cards and SARIF output — test servers before connecting them to an AI system.",
5
5
  "main": "engine.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "engine.js",
11
+ "vetokernel-client.js",
11
12
  "bin/mcp-proof.js",
12
13
  "README.md",
13
14
  "LICENSE"
@@ -0,0 +1,168 @@
1
+ // vetokernel-client.js — post an inspected tools/call to VetoKernel's
2
+ // execution-authorization endpoint and independently verify the receipt.
3
+ //
4
+ // Node stdlib only (http, https, crypto, url) — mcp-proof adds no dependencies.
5
+
6
+ 'use strict';
7
+
8
+ const crypto = require('crypto');
9
+ const http = require('http');
10
+ const https = require('https');
11
+ const { URL } = require('url');
12
+
13
+ const SCHEMA_VERSION = '1.0';
14
+
15
+ // goJSONString matches Go's encoding/json string encoding, which HTML-escapes
16
+ // <, > and & by default. Everything else Node's JSON.stringify already agrees on.
17
+ function goJSONString(value) {
18
+ return JSON.stringify(String(value))
19
+ .replace(/</g, '\\u003c')
20
+ .replace(/>/g, '\\u003e')
21
+ .replace(/&/g, '\\u0026');
22
+ }
23
+
24
+ // canonicalReceiptJSON reproduces, in JavaScript, the exact byte sequence
25
+ // VetoKernel hashes into receipt_hash (Go: backend/auth/execution_authorization.go
26
+ // CanonicalReceiptBytes). It is deliberately an INDEPENDENT re-implementation:
27
+ // agreement between the two is what makes the receipt independently verified
28
+ // rather than merely re-read. Field order and the omitempty set are load-bearing
29
+ // and are pinned by a golden vector in both test suites.
30
+ function canonicalReceiptJSON(receipt) {
31
+ const fields = [
32
+ ['schema_version', receipt.schema_version, false],
33
+ ['decision', receipt.decision, false],
34
+ ['action_id', receipt.action_id, false],
35
+ ['action_type', receipt.action_type, false],
36
+ ['target_id', receipt.target_id, false],
37
+ ['tool_name', receipt.tool_name, true],
38
+ ['parameters_hash', receipt.parameters_hash, true],
39
+ ['tenant_id', receipt.tenant_id, false],
40
+ ['payload_hash', receipt.payload_hash, false],
41
+ ['context_hash', receipt.context_hash, false],
42
+ ['authorized_by_agent_id', receipt.authorized_by_agent_id, true],
43
+ ['issued_at', receipt.issued_at, false],
44
+ ];
45
+ const parts = [];
46
+ for (const [key, raw, omitempty] of fields) {
47
+ const value = raw === undefined || raw === null ? '' : raw;
48
+ if (omitempty && value === '') continue;
49
+ parts.push(goJSONString(key) + ':' + goJSONString(value));
50
+ }
51
+ return '{' + parts.join(',') + '}';
52
+ }
53
+
54
+ // verifyReceipt re-derives both bindings the receipt claims: that payload_hash
55
+ // is sha256 of the exact parameter bytes we sent, and that receipt_hash is
56
+ // sha256 of the canonical receipt fields.
57
+ function verifyReceipt(receipt, parametersJSON) {
58
+ if (!receipt || typeof receipt !== 'object') {
59
+ return { verified: false, problems: ['no receipt returned'], payload_hash: '', receipt_hash: '' };
60
+ }
61
+ const problems = [];
62
+ if (receipt.schema_version !== SCHEMA_VERSION) {
63
+ problems.push(`unexpected schema_version ${receipt.schema_version}`);
64
+ }
65
+ if (receipt.decision !== 'allow') {
66
+ problems.push(`unexpected receipt decision ${receipt.decision}`);
67
+ }
68
+ const payloadHash = crypto.createHash('sha256')
69
+ .update(Buffer.from(String(parametersJSON), 'utf8')).digest('hex');
70
+ if (receipt.payload_hash !== payloadHash) {
71
+ problems.push(`payload_hash does not bind the parameters we sent (recomputed ${payloadHash})`);
72
+ }
73
+ const receiptHash = crypto.createHash('sha256')
74
+ .update(canonicalReceiptJSON(receipt), 'utf8').digest('hex');
75
+ if (receipt.receipt_hash !== receiptHash) {
76
+ problems.push(`receipt_hash mismatch (recomputed ${receiptHash})`);
77
+ }
78
+ return { verified: problems.length === 0, problems, payload_hash: payloadHash, receipt_hash: receiptHash };
79
+ }
80
+
81
+ // authorizeCall proposes a consequential action to VetoKernel and returns its
82
+ // decision. It fails closed: an allow whose receipt does not independently
83
+ // verify is downgraded to a deny.
84
+ function authorizeCall(options) {
85
+ const {
86
+ endpoint,
87
+ apiKey = '',
88
+ tenantId = 'default',
89
+ agentId = 'mcp-proof',
90
+ actionId = '',
91
+ toolName = '',
92
+ actionType = '',
93
+ targetId = '',
94
+ parametersJSON = '{}',
95
+ context,
96
+ scanner = { name: 'mcp-proof', decision: 'allow', severity: 'low', reason: 'clean' },
97
+ timeoutMs = 5000,
98
+ } = options;
99
+
100
+ const body = JSON.stringify({
101
+ schema_version: SCHEMA_VERSION,
102
+ action_id: actionId,
103
+ agent_id: agentId,
104
+ tool_name: toolName,
105
+ action_type: actionType,
106
+ target_id: targetId,
107
+ parameters_json: parametersJSON,
108
+ context: context || undefined,
109
+ scanner,
110
+ });
111
+
112
+ const url = new URL(endpoint);
113
+ const transport = url.protocol === 'https:' ? https : http;
114
+
115
+ return new Promise((resolve, reject) => {
116
+ const req = transport.request({
117
+ protocol: url.protocol,
118
+ hostname: url.hostname,
119
+ port: url.port,
120
+ path: url.pathname + url.search,
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ 'Content-Length': Buffer.byteLength(body),
125
+ 'X-API-Key': apiKey,
126
+ 'X-Tenant-ID': tenantId,
127
+ },
128
+ }, (res) => {
129
+ const chunks = [];
130
+ res.on('data', (c) => chunks.push(c));
131
+ res.on('end', () => {
132
+ const raw = Buffer.concat(chunks).toString('utf8');
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(raw);
136
+ } catch (_) {
137
+ reject(new Error(`vetokernel returned non-JSON (status ${res.statusCode}): ${raw.slice(0, 200)}`));
138
+ return;
139
+ }
140
+ const out = {
141
+ endpoint,
142
+ status: res.statusCode,
143
+ decision: parsed.decision === 'allow' ? 'allow' : 'deny',
144
+ reason: parsed.reason || '',
145
+ receipt: parsed.receipt || null,
146
+ verification: null,
147
+ };
148
+ out.verification = out.decision === 'allow'
149
+ ? verifyReceipt(parsed.receipt, parametersJSON)
150
+ : { verified: false, problems: ['denied — no receipt issued'], payload_hash: '', receipt_hash: '' };
151
+ if (out.decision === 'allow' && !out.verification.verified) {
152
+ out.decision = 'deny';
153
+ out.reason = `receipt failed independent verification: ${out.verification.problems.join('; ')}`;
154
+ }
155
+ resolve(out);
156
+ });
157
+ });
158
+
159
+ req.setTimeout(timeoutMs, () => {
160
+ req.destroy(new Error(`vetokernel request timed out after ${timeoutMs}ms`));
161
+ });
162
+ req.on('error', reject);
163
+ req.write(body);
164
+ req.end();
165
+ });
166
+ }
167
+
168
+ module.exports = { authorizeCall, verifyReceipt, canonicalReceiptJSON, SCHEMA_VERSION };