mcp-proof 0.3.2 → 0.3.4

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
@@ -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. *No live npm-registry API calls, no GitHub-API lookup, no signature verification.* These are planned but not in this release.
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: args.source || deriveSourceFromCommandLine(args.stdio),
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: args.source || (parsed.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: spec.source || deriveSourceFromCommand(spec.command, spec.args || []),
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
- res.trust = 'verified';
208
- res.integrityScore = 9.0;
209
- res.reason = `npm scope ${scope} is on the trusted publisher registry`;
210
- return res;
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.2",
3
+ "version": "0.3.4",
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": {
@@ -32,7 +32,7 @@
32
32
  "vyriai",
33
33
  "llm-security"
34
34
  ],
35
- "homepage": "https://github.com/rudranpatra/AI_MEMORY_CONTROL_PLANE/tree/main/tools/mcp-proof",
35
+ "homepage": "https://github.com/rudranpatra/AI_MEMORY_CONTROL_PLANE/tree/master/tools/mcp-proof",
36
36
  "author": "Vyriai <vyriaipro@gmail.com>",
37
37
  "license": "MIT"
38
38
  }