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 +31 -2
- package/bin/mcp-proof.js +58 -28
- package/package.json +2 -2
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
|
-
`
|
|
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>]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Unbranded, free MCP (Model Context Protocol) server verifier — detects tool poisoning, typosquats, hidden unicode, base64 payloads, exposed transports, and skill-manifest attacks.
|
|
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"
|