mcp-proof 0.3.7 → 0.3.8

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
@@ -28,6 +28,15 @@ npx mcp-proof --config ~/.config/Claude/claude_desktop_config.json
28
28
  # Scan a Claude SKILL.md for hidden attack instructions
29
29
  npx mcp-proof --skill ~/.claude/skills/example/SKILL.md
30
30
 
31
+ # Inspect an exact tools/call payload before execution
32
+ cat call.json | npx mcp-proof --execute --json
33
+
34
+ # Create a shareable Trust Card for a PR or README
35
+ npx mcp-proof tools.json --markdown > MCP_TRUST.md
36
+
37
+ # Emit SARIF for GitHub code scanning
38
+ npx mcp-proof tools.json --sarif > results.sarif
39
+
31
40
  # Or pipe stdin
32
41
  cat tools.json | npx mcp-proof --transport http --bind 0.0.0.0:3001
33
42
 
@@ -125,6 +134,10 @@ Options:
125
134
  --stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
126
135
  --config <path> scan every stdio server in an MCP client config with mcpServers
127
136
  --skill <path> scan a Claude SKILL.md (educational/warning context aware)
137
+ --execute [<path>] inspect a tools/call payload before execution
138
+ --markdown emit a shareable Markdown Trust Card
139
+ --trust-card alias for --markdown
140
+ --sarif emit SARIF 2.1.0 for GitHub code scanning
128
141
  --live confirm npm publisher on registry.npmjs.org (one network call)
129
142
  --timeout <ms> MCP launch/protocol timeout per server (default: 15000)
130
143
  --source <s> npm:@scope/pkg, git+https://..., file://...
@@ -140,9 +153,20 @@ Exit codes: 0 allow · 1 warn · 2 block · 64 usage · 65 parse error
140
153
 
141
154
  The non-zero exit code on `warn` / `block` makes this drop-in for CI: pin in your `package.json` and any tool poisoning fails the pipeline.
142
155
 
156
+ ## Trust Cards and GitHub SARIF
157
+
158
+ Every scan can produce a portable review artifact without uploading server metadata:
159
+
160
+ ```bash
161
+ npx mcp-proof --stdio "npx -y @modelcontextprotocol/server-filesystem /tmp" --markdown > MCP_TRUST.md
162
+ npx mcp-proof ./tools.json --sarif > mcp-proof-results.sarif
163
+ ```
164
+
165
+ `MCP_TRUST.md` is suitable for a pull request or repository README. SARIF can be uploaded to GitHub code scanning by a workflow or the included `action.yml`. The output is generated locally from the same verdict and includes the package version and findings; it is not a cryptographic authorization receipt.
166
+
143
167
  ## Real MCP scanning
144
168
 
145
- `0.3.0` can scan a live stdio MCP server, not just a saved JSON manifest. The CLI launches the server locally, performs the MCP `initialize` handshake, calls `tools/list`, and scans the returned tool metadata. It uses a timeout cap so a broken or malicious server cannot hang CI forever.
169
+ `mcp-proof` can scan a live stdio MCP server, not just a saved JSON manifest. The CLI launches the server locally, performs the MCP `initialize` handshake, calls `tools/list`, and scans the returned tool metadata. It uses a timeout cap so a broken or malicious server cannot hang CI forever.
146
170
 
147
171
  ```bash
148
172
  npx mcp-proof --stdio "npx -y @modelcontextprotocol/server-filesystem /tmp"
@@ -170,7 +194,7 @@ The scanner only supports stdio MCP servers in this release. HTTP/SSE runtime di
170
194
  ## Programmatic API
171
195
 
172
196
  ```js
173
- const { evaluate, scanSkillManifest } = require('mcp-proof');
197
+ const { evaluate, evaluateCall, scanSkillManifest } = require('mcp-proof');
174
198
 
175
199
  const result = evaluate({
176
200
  source: 'npm:@modelcontextprotocol/server-filesystem',
@@ -184,6 +208,10 @@ console.log(result.composite); // 0.0 – 10.0
184
208
  console.log(result.reasons); // human-readable explanations
185
209
  console.log(result.toolPoisoning.perTool); // per-tool findings
186
210
 
211
+ // Inspect an exact tool call before execution (does not execute or authorize it)
212
+ const call = evaluateCall({ tool: 'read_file', arguments: { path: './README.md' } });
213
+ console.log(call.decision);
214
+
187
215
  // Scan a Claude SKILL.md
188
216
  const skill = scanSkillManifest(fs.readFileSync('./SKILL.md', 'utf8'));
189
217
  console.log(skill.decision);
@@ -194,6 +222,7 @@ console.log(skill.decision);
194
222
  - **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.*
195
223
  - **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.
196
224
  - **No dependency CVE check.** The full gateway wraps `npm audit`; this CLI doesn't shell out.
225
+ - **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.
197
226
  - **Stdio discovery only.** `--stdio` / `--config` launches local stdio MCP servers and calls `tools/list`. It does not yet connect to HTTP/SSE MCP servers.
198
227
  - **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.
199
228
  - **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
@@ -51,7 +51,10 @@ Options:
51
51
  --stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
52
52
  --config <path> scan every stdio server in an MCP client config with mcpServers
53
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)
54
+ --execute [<path>] inspect a tools/call payload before execution (reads stdin if path omitted)
55
+ --markdown emit a shareable Markdown Trust Card
56
+ --trust-card alias for --markdown
57
+ --sarif emit SARIF 2.1.0 for GitHub code scanning
55
58
  --live confirm npm publisher against registry.npmjs.org (one network call)
56
59
  --timeout <ms> MCP launch/protocol timeout per server (default: 15000)
57
60
  --source <s> npm:@scope/pkg, git+https://..., or file://... (drives publisher verification)
@@ -105,11 +108,7 @@ async function main() {
105
108
  process.stderr.write(`mcp-proof: ${err.message}\n`);
106
109
  process.exit(err.exitCode || 65);
107
110
  }
108
- if (args.json) {
109
- process.stdout.write(JSON.stringify(output, null, 2) + '\n');
110
- } else {
111
- renderMulti(output);
112
- }
111
+ emitOutput(output, args);
113
112
  process.exit(exitCodeForDecision(output.decision));
114
113
  }
115
114
 
@@ -129,11 +128,7 @@ async function main() {
129
128
  process.stderr.write(`mcp-proof: ${err.message}\n`);
130
129
  process.exit(err.exitCode || 65);
131
130
  }
132
- if (args.json) {
133
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
134
- } else {
135
- renderHumanReadable(result, result.tool_count || 0, result.name || '');
136
- }
131
+ emitOutput(result, args);
137
132
  process.exit(exitCodeForDecision(result.decision));
138
133
  }
139
134
 
@@ -145,11 +140,7 @@ async function main() {
145
140
  process.stderr.write(`mcp-proof: ${err.message}\n`);
146
141
  process.exit(err.exitCode || 65);
147
142
  }
148
- if (args.json) {
149
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
150
- } else {
151
- renderHumanReadable(result, result.tool_count || 0, result.name || '');
152
- }
143
+ emitOutput(result, args);
153
144
  process.exit(exitCodeForDecision(result.decision));
154
145
  }
155
146
 
@@ -161,11 +152,7 @@ async function main() {
161
152
  process.stderr.write(`mcp-proof: ${err.message}\n`);
162
153
  process.exit(err.exitCode || 65);
163
154
  }
164
- if (args.json) {
165
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
166
- } else {
167
- renderHumanReadable(result, 0, result.tool || 'execute');
168
- }
155
+ emitOutput(result, args);
169
156
  process.exit(exitCodeForDecision(result.decision));
170
157
  }
171
158
 
@@ -211,11 +198,7 @@ async function main() {
211
198
  result.source = args.source || (parsed.source || '');
212
199
  result.tool_count = tools.length;
213
200
 
214
- if (args.json) {
215
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
216
- } else {
217
- renderHumanReadable(result, tools.length, result.name);
218
- }
201
+ emitOutput(result, args);
219
202
 
220
203
  process.exit(result.decision === 'allow' ? 0 : result.decision === 'warn' ? 1 : 2);
221
204
  }
@@ -224,14 +207,16 @@ function parseArgs(argv) {
224
207
  const out = {
225
208
  source: '', transport: '', bind: '', json: false, selfTest: false,
226
209
  help: false, version: false, file: '', stdio: '', config: '', skill: '',
227
- execute: false, live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
210
+ execute: false, format: 'human', live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
228
211
  };
229
212
  for (let i = 0; i < argv.length; i++) {
230
213
  const a = argv[i];
231
214
  switch (a) {
232
215
  case '-h': case '--help': out.help = true; break;
233
216
  case '-V': case '--version': out.version = true; break;
234
- case '--json': out.json = true; break;
217
+ case '--json': out.json = true; out.format = 'json'; break;
218
+ case '--markdown': case '--trust-card': out.format = 'markdown'; break;
219
+ case '--sarif': out.format = 'sarif'; break;
235
220
  case '--self-test': out.selfTest = true; break;
236
221
  case '--source': out.source = argv[++i] || ''; break;
237
222
  case '--transport': out.transport = argv[++i] || ''; break;
@@ -599,6 +584,51 @@ function worstDecision(decisions) {
599
584
  return 'allow';
600
585
  }
601
586
 
587
+ function emitOutput(result, args) {
588
+ if (args.format === 'json') {
589
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
590
+ } else if (args.format === 'sarif') {
591
+ process.stdout.write(JSON.stringify(toSarif(result), null, 2) + '\n');
592
+ } else if (args.format === 'markdown') {
593
+ process.stdout.write(toTrustCard(result) + '\n');
594
+ } else if (result.results) {
595
+ renderMulti(result);
596
+ } else {
597
+ renderHumanReadable(result, result.tool_count || 0, result.name || result.tool || '');
598
+ }
599
+ }
600
+
601
+ function toTrustCard(r) {
602
+ const verdict = String(r.decision || 'unknown').toUpperCase();
603
+ const source = r.source || r.name || 'stdin';
604
+ const findings = r.toolPoisoning && r.toolPoisoning.perTool
605
+ ? r.toolPoisoning.perTool.flatMap(t => (t.patterns || []).map(p => `- **${p.name}** (${p.severity}) — ${p.reason}`))
606
+ : r.call_scan && r.call_scan.detected ? [`- **runtime_payload** (${r.call_scan.severity}) — ${r.call_scan.reason}`] : [];
607
+ const hash = r.manifestHash || r.payloadHash || 'not emitted';
608
+ return [
609
+ '# MCP Trust Card', '',
610
+ `**Verdict: ${verdict}**`, '',
611
+ '| Field | Value |', '|---|---|',
612
+ `| Source | ${String(source).replace(/\|/g, '\\|')} |`,
613
+ `| Tools scanned | ${r.tool_count ?? 'n/a'} |`,
614
+ `| Trust score | ${r.composite ?? 'n/a'} / 10 |`,
615
+ `| Findings | ${findings.length} |`,
616
+ `| Evidence hash | ${hash} |`,
617
+ `| mcp-proof | ${require('../package.json').version} |`, '',
618
+ '## Findings', '', findings.length ? findings.join('\n') : '- No actionable findings.', '',
619
+ '> Reproduce locally with `npx mcp-proof --json`.'
620
+ ].join('\n');
621
+ }
622
+
623
+ function toSarif(r) {
624
+ const results = [];
625
+ const findings = r.toolPoisoning && r.toolPoisoning.perTool
626
+ ? r.toolPoisoning.perTool.flatMap(t => (t.patterns || []).map(p => ({ ruleId: p.name, level: p.severity === 'critical' || p.severity === 'high' ? 'error' : 'warning', message: `${p.reason} (tool: ${t.tool})` })))
627
+ : r.call_scan && r.call_scan.detected ? [{ ruleId: 'runtime_payload', level: r.call_scan.severity === 'critical' ? 'error' : 'warning', message: r.call_scan.reason }] : [];
628
+ for (const f of findings) results.push({ ruleId: f.ruleId, level: f.level, message: { text: f.message } });
629
+ return { version: '2.1.0', $schema: 'https://json.schemastore.org/sarif-2.1.0.json', runs: [{ tool: { driver: { name: 'mcp-proof', version: require('../package.json').version, rules: [...new Map(findings.map(f => [f.ruleId, { id: f.ruleId, shortDescription: { text: f.ruleId } }])).values()] } }, results }] };
630
+ }
631
+
602
632
  function runExecute(args) {
603
633
  const raw = args.file ? readFileLimited(args.file) : readStdinLimited();
604
634
  if (!raw.trim()) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-proof",
3
- "version": "0.3.7",
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.",
3
+ "version": "0.3.8",
4
+ "description": "Unbranded, free MCP (Model Context Protocol) server verifier with local Markdown Trust Cards and SARIF output — detects tool poisoning, typosquats, hidden unicode, base64 payloads, exposed transports, and skill-manifest attacks.",
5
5
  "main": "engine.js",
6
6
  "bin": {
7
7
  "mcp-proof": "bin/mcp-proof.js"