mcp-proof 0.3.2 → 0.3.3
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 +2 -1
- package/bin/mcp-proof.js +17 -5
- package/engine.js +56 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ Options:
|
|
|
125
125
|
--stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
|
|
126
126
|
--config <path> scan every stdio server in an MCP client config with mcpServers
|
|
127
127
|
--skill <path> scan a Claude SKILL.md (educational/warning context aware)
|
|
128
|
+
--live confirm npm publisher on registry.npmjs.org (one network call)
|
|
128
129
|
--timeout <ms> MCP launch/protocol timeout per server (default: 15000)
|
|
129
130
|
--source <s> npm:@scope/pkg, git+https://..., file://...
|
|
130
131
|
--transport <t> stdio | http | sse | http_streaming (default: stdio)
|
|
@@ -190,7 +191,7 @@ console.log(skill.decision);
|
|
|
190
191
|
|
|
191
192
|
## Limits (honest scope)
|
|
192
193
|
|
|
193
|
-
- **Publisher verification is lexical only.** npm scope match + Levenshtein typosquat.
|
|
194
|
+
- **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.*
|
|
194
195
|
- **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.
|
|
195
196
|
- **No dependency CVE check.** The full gateway wraps `npm audit`; this CLI doesn't shell out.
|
|
196
197
|
- **Stdio discovery only.** `--stdio` / `--config` launches local stdio MCP servers and calls `tools/list`. It does not yet connect to HTTP/SSE MCP servers.
|
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 } = require('../engine.js');
|
|
26
|
+
const { evaluate, scanSkillManifest, verifyPublisherAsync } = require('../engine.js');
|
|
27
27
|
|
|
28
28
|
const DEFAULT_MAX_INPUT_BYTES = 5 * 1024 * 1024;
|
|
29
29
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
@@ -50,6 +50,7 @@ Options:
|
|
|
50
50
|
--stdio <cmd> launch a real stdio MCP server, call tools/list, scan returned tools
|
|
51
51
|
--config <path> scan every stdio server in an MCP client config with mcpServers
|
|
52
52
|
--skill <path> scan a Claude SKILL.md (educational/warning context aware)
|
|
53
|
+
--live confirm npm publisher against registry.npmjs.org (one network call)
|
|
53
54
|
--timeout <ms> MCP launch/protocol timeout per server (default: 15000)
|
|
54
55
|
--source <s> npm:@scope/pkg, git+https://..., or file://... (drives publisher verification)
|
|
55
56
|
--transport <t> stdio | http | sse | http_streaming (default: stdio)
|
|
@@ -113,10 +114,13 @@ async function main() {
|
|
|
113
114
|
if (args.stdio) {
|
|
114
115
|
let result;
|
|
115
116
|
try {
|
|
117
|
+
const stdioSource = args.source || deriveSourceFromCommandLine(args.stdio);
|
|
118
|
+
const publisher = args.live ? await verifyPublisherAsync(stdioSource) : null;
|
|
116
119
|
result = await scanStdioCommand({
|
|
117
120
|
name: args.source || args.stdio,
|
|
118
121
|
commandLine: args.stdio,
|
|
119
|
-
source:
|
|
122
|
+
source: stdioSource,
|
|
123
|
+
publisher,
|
|
120
124
|
timeoutMs: args.timeoutMs,
|
|
121
125
|
});
|
|
122
126
|
} catch (err) {
|
|
@@ -175,11 +179,14 @@ async function main() {
|
|
|
175
179
|
process.exit(65);
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
const sourceForPublisher = args.source || (parsed.source || '');
|
|
183
|
+
const publisher = args.live ? await verifyPublisherAsync(sourceForPublisher) : null;
|
|
178
184
|
const result = evaluate({
|
|
179
|
-
source:
|
|
185
|
+
source: sourceForPublisher,
|
|
180
186
|
transport: args.transport || (parsed.transport || 'stdio'),
|
|
181
187
|
bind_address: args.bind || (parsed.bind_address || ''),
|
|
182
188
|
tools,
|
|
189
|
+
publisher,
|
|
183
190
|
});
|
|
184
191
|
result.mode = 'manifest';
|
|
185
192
|
result.name = args.file || 'stdin';
|
|
@@ -199,7 +206,7 @@ function parseArgs(argv) {
|
|
|
199
206
|
const out = {
|
|
200
207
|
source: '', transport: '', bind: '', json: false, selfTest: false,
|
|
201
208
|
help: false, version: false, file: '', stdio: '', config: '', skill: '',
|
|
202
|
-
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
209
|
+
live: false, timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
203
210
|
};
|
|
204
211
|
for (let i = 0; i < argv.length; i++) {
|
|
205
212
|
const a = argv[i];
|
|
@@ -214,6 +221,7 @@ function parseArgs(argv) {
|
|
|
214
221
|
case '--stdio': out.stdio = argv[++i] || ''; break;
|
|
215
222
|
case '--config': out.config = expandHome(argv[++i] || ''); break;
|
|
216
223
|
case '--skill': out.skill = expandHome(argv[++i] || ''); break;
|
|
224
|
+
case '--live': out.live = true; break;
|
|
217
225
|
case '--timeout': out.timeoutMs = parseTimeout(argv[++i] || ''); break;
|
|
218
226
|
default:
|
|
219
227
|
if (a.startsWith('-')) {
|
|
@@ -312,13 +320,16 @@ async function scanConfig(args) {
|
|
|
312
320
|
results.push(errorResult(name, 'config entry missing command'));
|
|
313
321
|
continue;
|
|
314
322
|
}
|
|
323
|
+
const serverSource = spec.source || deriveSourceFromCommand(spec.command, spec.args || []);
|
|
315
324
|
try {
|
|
325
|
+
const publisher = args.live ? await verifyPublisherAsync(serverSource) : null;
|
|
316
326
|
results.push(await scanStdioCommand({
|
|
317
327
|
name,
|
|
318
328
|
command: spec.command,
|
|
319
329
|
args: Array.isArray(spec.args) ? spec.args : [],
|
|
320
330
|
env: spec.env && typeof spec.env === 'object' ? spec.env : {},
|
|
321
|
-
source:
|
|
331
|
+
source: serverSource,
|
|
332
|
+
publisher,
|
|
322
333
|
timeoutMs: args.timeoutMs,
|
|
323
334
|
}));
|
|
324
335
|
} catch (err) {
|
|
@@ -349,6 +360,7 @@ async function scanStdioCommand(opts) {
|
|
|
349
360
|
transport: 'stdio',
|
|
350
361
|
bind_address: '',
|
|
351
362
|
tools,
|
|
363
|
+
publisher: opts.publisher || null,
|
|
352
364
|
});
|
|
353
365
|
result.mode = 'stdio';
|
|
354
366
|
result.name = opts.name || launch.command;
|
package/engine.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
+
const https = require('https');
|
|
15
|
+
|
|
14
16
|
// ── Tool-poisoning signatures ───────────────────────────────────────────
|
|
15
17
|
const SIGNATURES = [
|
|
16
18
|
{
|
|
@@ -202,12 +204,18 @@ function verifyPublisher(source) {
|
|
|
202
204
|
if (src.startsWith('npm:')) {
|
|
203
205
|
const pkg = src.slice('npm:'.length);
|
|
204
206
|
res.matchedRegistry = 'npm';
|
|
207
|
+
// A trusted scope only verifies packages that are actually on the
|
|
208
|
+
// known-name list. A package under a trusted scope but not on the list
|
|
209
|
+
// is either a typo or a new, unreviewed package, and is downgraded.
|
|
205
210
|
for (const scope of TRUSTED_NPM_SCOPES) {
|
|
206
211
|
if (pkg.startsWith(scope + '/')) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
if (KNOWN_NAMES.includes(pkg)) {
|
|
213
|
+
res.trust = 'verified';
|
|
214
|
+
res.integrityScore = 9.0;
|
|
215
|
+
res.reason = `npm package ${pkg} is on the trusted publisher registry`;
|
|
216
|
+
return res;
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
211
219
|
}
|
|
212
220
|
}
|
|
213
221
|
if (!KNOWN_NAMES.includes(pkg)) {
|
|
@@ -248,6 +256,48 @@ function verifyPublisher(source) {
|
|
|
248
256
|
return res;
|
|
249
257
|
}
|
|
250
258
|
|
|
259
|
+
function npmPackageExists(pkg, timeoutMs = 5000) {
|
|
260
|
+
return new Promise((resolve) => {
|
|
261
|
+
const encoded = encodeURIComponent(pkg).replace(/%40/g, '@');
|
|
262
|
+
const url = `https://registry.npmjs.org/${encoded}`;
|
|
263
|
+
const req = https.get(url, { timeout: timeoutMs }, (res) => {
|
|
264
|
+
res.on('data', () => {});
|
|
265
|
+
res.on('end', () => {
|
|
266
|
+
resolve(res.statusCode === 200 ? true : res.statusCode === 404 ? false : null);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
req.on('error', () => resolve(null));
|
|
270
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function verifyPublisherAsync(source) {
|
|
275
|
+
const res = verifyPublisher(source);
|
|
276
|
+
const src = (source || '').toLowerCase().trim();
|
|
277
|
+
if (src.startsWith('npm:')) {
|
|
278
|
+
const pkg = src.slice('npm:'.length);
|
|
279
|
+
const exists = await npmPackageExists(pkg);
|
|
280
|
+
if (exists === false) {
|
|
281
|
+
res.matchedRegistry = '';
|
|
282
|
+
res.trust = 'unverified';
|
|
283
|
+
res.integrityScore = Math.min(res.integrityScore, 2.0);
|
|
284
|
+
res.reason = `npm package "${pkg}" not found in the registry`;
|
|
285
|
+
} else if (exists === true) {
|
|
286
|
+
res.matchedRegistry = 'npm';
|
|
287
|
+
if (res.typosquat) {
|
|
288
|
+
res.reason = `${res.reason}; package exists but is a typosquat`;
|
|
289
|
+
} else if (res.trust !== 'verified') {
|
|
290
|
+
res.trust = 'verified';
|
|
291
|
+
res.integrityScore = Math.max(res.integrityScore, 8.0);
|
|
292
|
+
res.reason = res.reason === 'unrecognised source format'
|
|
293
|
+
? `npm package "${pkg}" confirmed on registry`
|
|
294
|
+
: `${res.reason} — confirmed on npm registry`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return res;
|
|
299
|
+
}
|
|
300
|
+
|
|
251
301
|
function hasSuspiciousBase64(s) {
|
|
252
302
|
const matches = s.match(BASE64_RE);
|
|
253
303
|
if (!matches) return false;
|
|
@@ -408,7 +458,7 @@ function validateTransport(transport, bind) {
|
|
|
408
458
|
function evaluate(req) {
|
|
409
459
|
const start = Date.now();
|
|
410
460
|
const score = {
|
|
411
|
-
publisher: verifyPublisher(req.source),
|
|
461
|
+
publisher: req.publisher || verifyPublisher(req.source),
|
|
412
462
|
toolPoisoning: scanTools(req.tools || [], { skillContext: !!req.skillContext }),
|
|
413
463
|
transport: validateTransport(req.transport, req.bind_address),
|
|
414
464
|
reasons: [],
|
|
@@ -472,4 +522,4 @@ function evaluate(req) {
|
|
|
472
522
|
|
|
473
523
|
function round1(v) { return Math.round(v * 10) / 10; }
|
|
474
524
|
|
|
475
|
-
module.exports = { evaluate, scanTools, scanSkillManifest, verifyPublisher, validateTransport, levenshtein };
|
|
525
|
+
module.exports = { evaluate, scanTools, scanSkillManifest, verifyPublisher, verifyPublisherAsync, validateTransport, levenshtein };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-proof",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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": {
|