mcp-proof 0.3.12 → 0.3.14

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 ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.3.14
4
+
5
+ Severity-aware tool-poisoning summaries distinguish actionable patterns from low-severity URL mentions. Stdio launch errors now preserve server stderr for root-cause triage.
6
+
7
+ ## 0.3.13
8
+
9
+ Scanner-only release. Runtime authorization functionality is not included.
10
+
11
+ The package contains MCP tool-poisoning, hidden Unicode, typosquat, transport,
12
+ runtime tools/call, Claude Skill, client-config, SARIF, Trust Card, payload-size,
13
+ and live stdio scanning capabilities.
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 (and, with --vetokernel-endpoint, VetoKernel authorized it)
12
+ // 0 — verdict allow
13
13
  // 1 — verdict warn
14
- // 2 — verdict block, or VetoKernel denied the action
14
+ // 2 — verdict block
15
15
  // 64 — usage error
16
16
  // 65 — input parse error
17
17
  //
@@ -24,7 +24,6 @@ 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');
28
27
 
29
28
  const DEFAULT_MAX_INPUT_BYTES = 5 * 1024 * 1024;
30
29
  const DEFAULT_TIMEOUT_MS = 15000;
@@ -54,9 +53,6 @@ Options:
54
53
  --skill <path> scan a Claude SKILL.md (educational/warning context aware)
55
54
  --inspect-call [<path>] inspect a tools/call payload before execution (reads stdin if path omitted)
56
55
  --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)
60
56
  --markdown emit a shareable Markdown Trust Card
61
57
  --trust-card alias for --markdown
62
58
  --sarif emit SARIF 2.1.0 for GitHub code scanning
@@ -156,23 +152,13 @@ async function main() {
156
152
  let result;
157
153
  let call;
158
154
  try {
159
- ({ result, call } = runExecute(args));
155
+ ({ result } = runExecute(args));
160
156
  } catch (err) {
161
157
  process.stderr.write(`mcp-proof: ${err.message}\n`);
162
158
  process.exit(err.exitCode || 65);
163
159
  }
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
- }
174
160
  emitOutput(result, args);
175
- process.exit(exitCodeForInspectedCall(result));
161
+ process.exit(exitCodeForDecision(result.decision));
176
162
  }
177
163
 
178
164
  let raw;
@@ -227,7 +213,6 @@ function parseArgs(argv) {
227
213
  source: '', transport: '', bind: '', json: false, selfTest: false,
228
214
  help: false, version: false, file: '', stdio: '', config: '', skill: '',
229
215
  inspectCall: false, deprecatedExecute: false, format: 'human', live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
230
- vetokernelEndpoint: process.env.VETOKERNEL_ENDPOINT || '',
231
216
  };
232
217
  for (let i = 0; i < argv.length; i++) {
233
218
  const a = argv[i];
@@ -255,7 +240,6 @@ function parseArgs(argv) {
255
240
  break;
256
241
  case '--live': out.live = true; break;
257
242
  case '--timeout': out.timeoutMs = parseTimeout(argv[++i] || ''); break;
258
- case '--vetokernel-endpoint': out.vetokernelEndpoint = argv[++i] || ''; break;
259
243
  default:
260
244
  if (a.startsWith('-')) {
261
245
  process.stderr.write(`mcp-proof: unknown flag ${a}\n`);
@@ -415,10 +399,14 @@ async function fetchToolsList({ command, args, env, timeoutMs }) {
415
399
  windowsHide: true,
416
400
  });
417
401
 
418
- const timer = setTimeout(() => fail(new Error(`MCP server timed out after ${timeoutMs}ms`)), timeoutMs);
402
+ const timer = setTimeout(() => {
403
+ const tail = stderrBuf.trim() ? ` — stderr: ${stderrBuf.trim().slice(-500)}` : '';
404
+ fail(new Error(`MCP server timed out after ${timeoutMs}ms${tail}`));
405
+ }, timeoutMs);
419
406
 
407
+ let stderrBuf = '';
420
408
  child.on('error', fail);
421
- child.stderr.on('data', () => {});
409
+ child.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf8'); });
422
410
  child.stdout.on('data', (chunk) => {
423
411
  buffer = Buffer.concat([buffer, chunk]);
424
412
  buffer = consumeMessages(buffer, (msg) => {
@@ -442,9 +430,14 @@ async function fetchToolsList({ command, args, env, timeoutMs }) {
442
430
  }
443
431
  });
444
432
  });
433
+ function childErrorContext(base, code, signal) {
434
+ const tail = stderrBuf.trim() ? ` — stderr: ${stderrBuf.trim().slice(-500)}` : '';
435
+ return new Error(`${base} (code=${code}, signal=${signal})${tail}`);
436
+ }
437
+
445
438
  child.on('exit', (code, signal) => {
446
439
  if (!settled && pending.size > 0) {
447
- fail(new Error(`MCP server exited before tools/list completed (code=${code}, signal=${signal})`));
440
+ fail(childErrorContext('MCP server exited before tools/list completed', code, signal));
448
441
  }
449
442
  });
450
443
 
@@ -671,40 +664,7 @@ function runExecute(args) {
671
664
  const result = evaluateCall(parsed);
672
665
  result.mode = 'inspect-call';
673
666
  result.name = args.file || 'stdin';
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);
667
+ return { result };
708
668
  }
709
669
 
710
670
  function exitCodeForDecision(d) {
package/engine.js CHANGED
@@ -423,6 +423,37 @@ function hasSuspiciousBase64(s) {
423
423
  return false;
424
424
  }
425
425
 
426
+ function summarizePatterns(perTool, severity) {
427
+ const counts = {};
428
+ for (const t of perTool) {
429
+ for (const p of t.patterns) {
430
+ if (p.severity === severity) counts[p.name] = (counts[p.name] || 0) + 1;
431
+ }
432
+ }
433
+ const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
434
+ return entries.map(([name, count]) => count > 1 ? `${count} ${name} mentions` : `1 ${name} pattern`);
435
+ }
436
+
437
+ function formatToolPoisoningReason(perTool) {
438
+ if (perTool.length === 0) return 'no instruction-hijacking patterns detected';
439
+ const sevOrder = ['critical', 'high', 'medium', 'low'];
440
+ const bySev = {};
441
+ for (const t of perTool) {
442
+ for (const p of t.patterns) {
443
+ bySev[p.severity] = bySev[p.severity] || [];
444
+ bySev[p.severity].push({ patterns: [p] });
445
+ }
446
+ }
447
+ const parts = [];
448
+ for (const sev of sevOrder) {
449
+ const list = bySev[sev];
450
+ if (!list || list.length === 0) continue;
451
+ const names = summarizePatterns(list, sev);
452
+ parts.push(`${names.join(', ')} at ${sev} severity`);
453
+ }
454
+ return parts.join('; ');
455
+ }
456
+
426
457
  function scanTools(tools, opts) {
427
458
  const skillContext = (opts || {}).skillContext === true;
428
459
  if (!tools || tools.length === 0) {
@@ -499,9 +530,7 @@ function scanTools(tools, opts) {
499
530
  return {
500
531
  detected: perTool.length > 0,
501
532
  severity: maxSev,
502
- reason: perTool.length > 0
503
- ? `${perTool.length}/${tools.length} tools contain instruction-hijacking or sensitive-path references`
504
- : 'no instruction-hijacking patterns detected',
533
+ reason: formatToolPoisoningReason(perTool),
505
534
  perTool,
506
535
  };
507
536
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proof",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
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,9 +8,9 @@
8
8
  },
9
9
  "files": [
10
10
  "engine.js",
11
- "vetokernel-client.js",
12
11
  "bin/mcp-proof.js",
13
12
  "README.md",
13
+ "CHANGELOG.md",
14
14
  "LICENSE"
15
15
  ],
16
16
  "engines": {
@@ -1,168 +0,0 @@
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 };