agentverify 1.6.0 → 2.0.0

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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to the `agentverify` CLI/SDK package are documented here.
4
4
 
5
+ ## [2.0.0] - 2026-10-30
6
+
7
+ ### Added
8
+
9
+ - `agentverify mcp`: run Agent Verify as an MCP server (stdio) for Claude Code, Cursor, Windsurf and VS Code.
10
+ Tools: `scan_path`, `scan_code`, `scan_mcp_config`. Reads `AGENTVERIFY_API_KEY`.
11
+ `claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp`
12
+ - Folder scans from the MCP server are combined into one report (60 files / 5MB, agent files first, tests last) and
13
+ each finding is mapped back to its file and line.
14
+ - MCP config checks read only the server definitions and mask secret values locally before sending.
15
+ - `Finding.line` is now part of the SDK type.
16
+ - `agentverify/guard`: Agent Guard runtime protection (`createGuard`, `wrap`, `wrapAll`, `GuardBlockedError`).
17
+
5
18
  ## [1.6.0] - 2026-10-27
6
19
 
7
20
  ### Added
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  "use strict";
3
3
 
4
4
  // src/cli.ts
5
- var import_fs2 = require("fs");
6
- var import_path = require("path");
5
+ var import_fs4 = require("fs");
6
+ var import_path3 = require("path");
7
7
 
8
8
  // src/sdk.ts
9
9
  var DEFAULT_API_URL = "https://agentverify-api.agentverify.workers.dev/v1/scan";
@@ -115,8 +115,393 @@ async function scan(input, options) {
115
115
  }
116
116
  }
117
117
 
118
- // src/profile.ts
118
+ // src/mcp.ts
119
+ var import_readline = require("readline");
120
+ var import_fs2 = require("fs");
121
+ var import_path2 = require("path");
122
+
123
+ // src/files.ts
119
124
  var import_fs = require("fs");
125
+ var import_path = require("path");
126
+ var import_os = require("os");
127
+ var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
128
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache", ".venv", "venv", "__pycache__", "out", "target"]);
129
+ var SKIP_FILES = /* @__PURE__ */ new Set(["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Cargo.lock", "composer.lock", "tsconfig.json"]);
130
+ var MAX_FILES = 60;
131
+ var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
132
+ var MAX_FILE_BYTES = 400 * 1024;
133
+ function filePriority(relPath) {
134
+ const p = relPath.toLowerCase().replace(/\\/g, "/");
135
+ const name = p.split("/").pop() ?? p;
136
+ let score = 0;
137
+ if (/(^|\/)(tests?|__tests__|spec|e2e|fixtures?|examples?)\//.test(p) || /\.(test|spec)\.[a-z]+$/.test(name)) score -= 80;
138
+ if (/(agent|tool|mcp|prompt|crew|graph|server|config|policy|skill)/.test(name)) score += 30;
139
+ if (/(^|\/)(agents?|tools?|mcp|src|app|lib)\//.test(p)) score += 10;
140
+ if (name.endsWith(".md")) score -= 20;
141
+ if (name === "skill.md" || name === "agents.md" || name === "claude.md") score += 40;
142
+ return score - p.split("/").length;
143
+ }
144
+ function walk(dir, out) {
145
+ let entries;
146
+ try {
147
+ entries = (0, import_fs.readdirSync)(dir);
148
+ } catch {
149
+ return;
150
+ }
151
+ for (const entry of entries) {
152
+ if (SKIP_DIRS.has(entry) || SKIP_FILES.has(entry)) continue;
153
+ const full = (0, import_path.join)(dir, entry);
154
+ let st;
155
+ try {
156
+ st = (0, import_fs.statSync)(full);
157
+ } catch {
158
+ continue;
159
+ }
160
+ if (st.isDirectory()) walk(full, out);
161
+ else if (SCANNABLE_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) out.push(full);
162
+ }
163
+ }
164
+ function pickFiles(target) {
165
+ const notes = [];
166
+ let st;
167
+ try {
168
+ st = (0, import_fs.statSync)(target);
169
+ } catch {
170
+ throw new Error(`Path not found: ${target}`);
171
+ }
172
+ if (st.isFile()) {
173
+ if (st.size > MAX_TOTAL_BYTES) throw new Error(`${(0, import_path.basename)(target)} is larger than 5MB, which is the most one scan can take.`);
174
+ return { files: [{ path: (0, import_path.basename)(target), content: (0, import_fs.readFileSync)(target, "utf8") }], notes };
175
+ }
176
+ const all = [];
177
+ walk(target, all);
178
+ if (all.length === 0) throw new Error(`No agent files found in ${target}. Agent Verify reads ${[...SCANNABLE_EXTENSIONS].join(" ")} files.`);
179
+ const ranked = all.map((full) => ({ full, rel: (0, import_path.relative)(target, full).split(import_path.sep).join("/") })).sort((a, b) => filePriority(b.rel) - filePriority(a.rel) || a.rel.localeCompare(b.rel));
180
+ const files = [];
181
+ let total = 0;
182
+ let tooBig = 0;
183
+ for (const { full, rel } of ranked) {
184
+ if (files.length >= MAX_FILES) break;
185
+ let size = 0;
186
+ try {
187
+ size = (0, import_fs.statSync)(full).size;
188
+ } catch {
189
+ continue;
190
+ }
191
+ if (size > MAX_FILE_BYTES) {
192
+ tooBig++;
193
+ continue;
194
+ }
195
+ if (total + size > MAX_TOTAL_BYTES) break;
196
+ files.push({ path: rel, content: (0, import_fs.readFileSync)(full, "utf8") });
197
+ total += size;
198
+ }
199
+ const left = ranked.length - files.length - tooBig;
200
+ if (left > 0) notes.push(`${left} lower-priority file(s) were left out to stay within one scan (${MAX_FILES} files / 5MB).`);
201
+ if (tooBig > 0) notes.push(`${tooBig} file(s) over 400KB were skipped.`);
202
+ return { files, notes };
203
+ }
204
+ function combineFiles(files) {
205
+ if (files.length === 1) return files[0].content;
206
+ return files.map((f) => `// ===== FILE: ${f.path} =====
207
+ ${f.content}`).join("\n\n");
208
+ }
209
+ function locateLine(files, combinedLine) {
210
+ if (files.length === 1) return { path: files[0].path, line: combinedLine };
211
+ let cursor = 1;
212
+ for (let i = 0; i < files.length; i++) {
213
+ const start = cursor + 1;
214
+ const lines = files[i].content.split("\n").length;
215
+ if (combinedLine >= start && combinedLine < start + lines) return { path: files[i].path, line: combinedLine - start + 1 };
216
+ cursor = start + lines + 1;
217
+ }
218
+ return null;
219
+ }
220
+ function knownMcpConfigPaths(cwd = process.cwd()) {
221
+ const home = (0, import_os.homedir)();
222
+ const appData = process.env.APPDATA ?? (0, import_path.join)(home, "AppData", "Roaming");
223
+ const claudeDesktop = (0, import_os.platform)() === "win32" ? (0, import_path.join)(appData, "Claude", "claude_desktop_config.json") : (0, import_os.platform)() === "darwin" ? (0, import_path.join)(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : (0, import_path.join)(home, ".config", "Claude", "claude_desktop_config.json");
224
+ return [
225
+ { client: "Claude Code (this project)", path: (0, import_path.join)(cwd, ".mcp.json") },
226
+ { client: "Claude Code (user)", path: (0, import_path.join)(home, ".claude.json") },
227
+ { client: "Claude Desktop", path: claudeDesktop },
228
+ { client: "Cursor (this project)", path: (0, import_path.join)(cwd, ".cursor", "mcp.json") },
229
+ { client: "Cursor (user)", path: (0, import_path.join)(home, ".cursor", "mcp.json") },
230
+ { client: "VS Code (this project)", path: (0, import_path.join)(cwd, ".vscode", "mcp.json") },
231
+ { client: "Windsurf", path: (0, import_path.join)(home, ".codeium", "windsurf", "mcp_config.json") }
232
+ ];
233
+ }
234
+ function extractMcpServers(raw) {
235
+ let data;
236
+ try {
237
+ data = JSON.parse(raw);
238
+ } catch {
239
+ return null;
240
+ }
241
+ if (!data || typeof data !== "object") return null;
242
+ const servers = {};
243
+ const take = (obj, prefix = "") => {
244
+ if (!obj || typeof obj !== "object") return;
245
+ for (const key of ["mcpServers", "servers"]) {
246
+ const block = obj[key];
247
+ if (block && typeof block === "object" && !Array.isArray(block)) {
248
+ for (const [name, def] of Object.entries(block)) servers[prefix + name] = def;
249
+ }
250
+ }
251
+ };
252
+ take(data);
253
+ const projects = data.projects;
254
+ if (projects && typeof projects === "object") {
255
+ for (const [proj, value] of Object.entries(projects)) take(value, `${(0, import_path.basename)(proj)}/`);
256
+ }
257
+ return Object.keys(servers).length ? servers : null;
258
+ }
259
+ var SECRET_NAME = /(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PAT|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
260
+ var SECRET_VALUE = /^(sk-|sk_|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|npm_|glpat-|eyJ)/;
261
+ function mask(value) {
262
+ return value.length <= 4 ? "****redacted" : `${value.slice(0, 4)}****redacted`;
263
+ }
264
+ function redactMcpSecrets(servers) {
265
+ const clean = (node, parentKey = "") => {
266
+ if (typeof node === "string") {
267
+ if (/^\$\{|^<|^\$[A-Z_]/.test(node)) return node;
268
+ if (SECRET_NAME.test(parentKey) || SECRET_VALUE.test(node)) return mask(node);
269
+ const flag = node.match(/^(--?[A-Za-z0-9_-]*(?:token|key|secret|password|auth)[A-Za-z0-9_-]*=)(.+)$/i);
270
+ if (flag) return flag[1] + mask(flag[2]);
271
+ return node;
272
+ }
273
+ if (Array.isArray(node)) return node.map((item) => clean(item, parentKey));
274
+ if (node && typeof node === "object") {
275
+ const out = {};
276
+ for (const [k, v] of Object.entries(node)) out[k] = clean(v, k);
277
+ return out;
278
+ }
279
+ return node;
280
+ };
281
+ return clean(servers);
282
+ }
283
+ function existingMcpConfigs(cwd = process.cwd()) {
284
+ return knownMcpConfigPaths(cwd).filter((c2) => (0, import_fs.existsSync)(c2.path));
285
+ }
286
+
287
+ // src/mcp.ts
288
+ var SUPPORTED_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
289
+ var PRICING_URL = "https://aimodularity.com/agentverify/pricing/";
290
+ var DASHBOARD_URL = "https://aimodularity.com/agentverify/dashboard/";
291
+ var TOOLS = [
292
+ {
293
+ name: "scan_path",
294
+ title: "Scan agent files",
295
+ description: "Scan an AI agent for security risks before it is deployed: dangerous tool permissions, prompt injection paths, exposed secrets, risky MCP servers, missing approval gates, SQL/SSRF/code-execution risks, and framework-specific issues (LangChain, CrewAI, AutoGen, OpenAI Agents SDK, Claude Agent SDK). Give a file or a folder; a folder is scanned as one report using the most important agent files (up to 60 files / 5MB). Returns a verdict, a 0-100 security score (higher is safer) and findings with file and line.",
296
+ inputSchema: {
297
+ type: "object",
298
+ properties: { path: { type: "string", description: "File or folder to scan. Relative paths are resolved from the current project." } },
299
+ required: ["path"],
300
+ additionalProperties: false
301
+ }
302
+ },
303
+ {
304
+ name: "scan_code",
305
+ title: "Scan agent code",
306
+ description: "Scan a snippet of agent code, a tool definition, a system prompt or an MCP config for security risks. Use this for code that is not saved to a file yet. Returns a verdict, a security score and findings.",
307
+ inputSchema: {
308
+ type: "object",
309
+ properties: {
310
+ content: { type: "string", description: "The code or config to scan." },
311
+ fileName: { type: "string", description: "A file name with the right extension (e.g. agent.py, mcp.json) so the language is detected." }
312
+ },
313
+ required: ["content"],
314
+ additionalProperties: false
315
+ }
316
+ },
317
+ {
318
+ name: "scan_mcp_config",
319
+ title: "Check my MCP servers",
320
+ description: "Check the MCP servers configured on this computer (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf) for risks such as broad filesystem access, unpinned packages, remote servers without auth, auto-approved tools and API keys stored in plain text. Only the server definitions are read, and secret values are masked on this computer before anything is sent.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: { path: { type: "string", description: "Optional: a specific MCP config file. Leave empty to check all known locations." } },
324
+ additionalProperties: false
325
+ }
326
+ }
327
+ ];
328
+ var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
329
+ var SEVERITY_LABEL = { critical: "CRITICAL", high: "HIGH", medium: "MEDIUM", low: "LOW" };
330
+ function verdictLine(result) {
331
+ const verdict = result.verdict === "VERIFIED" ? "VERIFIED" : result.verdict === "NOT_ASSESSED" ? "NOT ASSESSED" : "NOT VERIFIED";
332
+ return `**Verdict: ${verdict}** \u2014 Security score ${result.riskScore}/100 (higher is safer, ${result.riskLevel})`;
333
+ }
334
+ function fixText(f) {
335
+ if (f.fixLocked) return `Fix: included with Agent Verify Pro (${PRICING_URL})`;
336
+ const steps = (f.recommendedFix ?? "").trim();
337
+ const code = (f.quickFix ?? f.fixCode ?? "").trim();
338
+ if (!steps && !code) return null;
339
+ return [steps ? `Fix: ${steps}` : "", code ? "```\n" + code + "\n```" : ""].filter(Boolean).join("\n");
340
+ }
341
+ function formatReport(result, opts) {
342
+ const findings = [...result.findings ?? []].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
343
+ const counts = ["critical", "high", "medium", "low"].map((s) => [s, findings.filter((f) => f.severity === s).length]).filter(([, n]) => n > 0);
344
+ const lines = [`# Agent Verify: ${opts.title}`, "", verdictLine(result)];
345
+ lines.push(findings.length ? `Findings: ${counts.map(([s, n]) => `${n} ${s}`).join(", ")}` : "No findings.");
346
+ if (result.reportInsights?.topBlocker) lines.push(`Fix first: ${result.reportInsights.topBlocker}`);
347
+ for (const note of opts.notes ?? []) lines.push(`Note: ${note}`);
348
+ findings.forEach((f, i) => {
349
+ const where = typeof f.line === "number" && f.line > 0 ? opts.files ? locateLine(opts.files, f.line) : { path: opts.title, line: f.line } : null;
350
+ lines.push("", `## ${i + 1}. [${SEVERITY_LABEL[f.severity] ?? f.severity}] ${f.title} (${f.code})`);
351
+ if (where) lines.push(`Location: ${where.path}:${where.line}`);
352
+ if (f.evidence) lines.push(`Evidence: ${f.evidence}`);
353
+ lines.push(`What is wrong: ${f.whatIsWrong}`);
354
+ if (f.whyItMatters) lines.push(`Why it matters: ${f.whyItMatters}`);
355
+ const fix = fixText(f);
356
+ if (fix) lines.push(fix);
357
+ });
358
+ const locked = findings.filter((f) => f.fixLocked).length;
359
+ if (locked > 0) lines.push("", `Step-by-step fixes and corrected code for ${locked} finding(s) are included with Pro: ${PRICING_URL}`);
360
+ if (result.reportUrl) lines.push("", `Full report: ${result.reportUrl}`);
361
+ lines.push("", "Agent Verify is an automated, evidence-based check, not a guarantee. Re-scan after making changes.");
362
+ return lines.join("\n");
363
+ }
364
+ function textResult(text, isError = false, structured) {
365
+ return { content: [{ type: "text", text }], ...isError ? { isError: true } : {}, ...structured ? { structuredContent: structured } : {} };
366
+ }
367
+ function summary(result) {
368
+ return {
369
+ verdict: result.verdict,
370
+ securityScore: result.riskScore,
371
+ riskLevel: result.riskLevel,
372
+ findings: (result.findings ?? []).map((f) => ({ code: f.code, severity: f.severity, title: f.title, line: f.line ?? null })),
373
+ reportUrl: result.reportUrl ?? null
374
+ };
375
+ }
376
+ async function callTool(name, args, opts) {
377
+ const tool = TOOLS.find((t) => t.name === name);
378
+ if (!tool) return textResult(`Unknown tool: ${name}`, true);
379
+ if (!opts.apiKey) {
380
+ return textResult(
381
+ "Agent Verify needs an API key. Create a free one in the dashboard (API / CLI tab): " + DASHBOARD_URL + "\nThen set AGENTVERIFY_API_KEY in this MCP server's environment and restart it.",
382
+ true
383
+ );
384
+ }
385
+ const doScan = opts.scanFn ?? scan;
386
+ const cwd = opts.cwd ?? process.cwd();
387
+ try {
388
+ if (name === "scan_code") {
389
+ const content2 = typeof args.content === "string" ? args.content : "";
390
+ if (!content2.trim()) return textResult('Nothing to scan: "content" is empty.', true);
391
+ const fileName = typeof args.fileName === "string" && args.fileName.trim() ? args.fileName.trim() : "agent";
392
+ const result2 = await doScan({ content: content2, fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
393
+ return textResult(formatReport(result2, { title: fileName }), false, summary(result2));
394
+ }
395
+ if (name === "scan_path") {
396
+ const raw = typeof args.path === "string" ? args.path.trim() : "";
397
+ if (!raw) return textResult('Give a file or folder "path" to scan.', true);
398
+ const target = (0, import_path2.resolve)(cwd, raw);
399
+ const picked = pickFiles(target);
400
+ const fileName = picked.files.length === 1 ? picked.files[0].path : `${raw.replace(/[\\/]+$/, "") || "project"} (${picked.files.length} files)`;
401
+ const result2 = await doScan({ content: combineFiles(picked.files), fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
402
+ const notes = [...picked.notes];
403
+ if (picked.files.length > 1) notes.unshift(`Scanned ${picked.files.length} files together as one report.`);
404
+ return textResult(formatReport(result2, { title: fileName, files: picked.files, notes }), false, summary(result2));
405
+ }
406
+ const explicit = typeof args.path === "string" && args.path.trim() ? (0, import_path2.resolve)(cwd, args.path.trim()) : "";
407
+ const configs = explicit ? [{ client: "Config file", path: explicit }] : existingMcpConfigs(cwd);
408
+ if (configs.length === 0) return textResult('No MCP config files were found in the usual places (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf). Pass a "path" to check a specific file.');
409
+ const servers = {};
410
+ const read = [];
411
+ for (const cfg of configs) {
412
+ let rawText = "";
413
+ try {
414
+ rawText = (0, import_fs2.readFileSync)(cfg.path, "utf8");
415
+ } catch {
416
+ continue;
417
+ }
418
+ const found = extractMcpServers(rawText);
419
+ if (!found) continue;
420
+ read.push(`${cfg.client}: ${Object.keys(found).length} server(s)`);
421
+ for (const [server, def] of Object.entries(found)) servers[`${cfg.client} \u203A ${server}`] = def;
422
+ }
423
+ if (Object.keys(servers).length === 0) return textResult(`Found ${configs.length} config file(s), but none of them define any MCP servers.`);
424
+ const content = JSON.stringify({ mcpServers: redactMcpSecrets(servers) }, null, 2);
425
+ const result = await doScan({ content, fileName: "mcp.json", platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
426
+ return textResult(
427
+ formatReport(result, { title: `MCP servers (${Object.keys(servers).length})`, notes: [`Checked: ${read.join("; ")}`, "Secret values were masked on this computer before scanning."] }),
428
+ false,
429
+ summary(result)
430
+ );
431
+ } catch (err) {
432
+ return textResult(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
433
+ }
434
+ }
435
+ async function handleMessage(msg, opts) {
436
+ const isNotification = msg.id === void 0 || msg.id === null;
437
+ const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: msg.id, result };
438
+ const fail = (code, message) => isNotification ? null : { jsonrpc: "2.0", id: msg.id ?? null, error: { code, message } };
439
+ switch (msg.method) {
440
+ case "initialize": {
441
+ const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : "";
442
+ return reply({
443
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(requested) ? requested : SUPPORTED_PROTOCOLS[0],
444
+ capabilities: { tools: { listChanged: false } },
445
+ serverInfo: { name: "agentverify", title: "Agent Verify", version: opts.version },
446
+ instructions: "Use Agent Verify to check AI agent code, tools, prompts and MCP configs for security risks before deploying. Run scan_path on the agent folder after making changes, and fix critical and high findings first."
447
+ });
448
+ }
449
+ case "ping":
450
+ return reply({});
451
+ case "tools/list":
452
+ return reply({ tools: TOOLS });
453
+ case "tools/call": {
454
+ const name = typeof msg.params?.name === "string" ? msg.params.name : "";
455
+ const args = msg.params?.arguments && typeof msg.params.arguments === "object" ? msg.params.arguments : {};
456
+ return reply(await callTool(name, args, opts));
457
+ }
458
+ case "resources/list":
459
+ return reply({ resources: [] });
460
+ case "prompts/list":
461
+ return reply({ prompts: [] });
462
+ default:
463
+ if (typeof msg.method === "string" && msg.method.startsWith("notifications/")) return null;
464
+ return fail(-32601, `Method not found: ${msg.method}`);
465
+ }
466
+ }
467
+ function runMcpServer(opts) {
468
+ return new Promise((done) => {
469
+ const rl = (0, import_readline.createInterface)({ input: process.stdin, crlfDelay: Infinity });
470
+ const send = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
471
+ let pending = 0;
472
+ let closed = false;
473
+ const maybeDone = () => {
474
+ if (closed && pending === 0) done();
475
+ };
476
+ rl.on("line", (line) => {
477
+ const text = line.trim();
478
+ if (!text) return;
479
+ let msg;
480
+ try {
481
+ msg = JSON.parse(text);
482
+ } catch {
483
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
484
+ return;
485
+ }
486
+ pending++;
487
+ handleMessage(msg, opts).then((res) => {
488
+ if (res) send(res);
489
+ }).catch((err) => send({ jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: err instanceof Error ? err.message : "Internal error" } })).finally(() => {
490
+ pending--;
491
+ maybeDone();
492
+ });
493
+ });
494
+ rl.on("close", () => {
495
+ closed = true;
496
+ maybeDone();
497
+ });
498
+ process.stderr.write(`Agent Verify MCP server ${opts.version} ready${opts.apiKey ? "" : " (no API key set: tools will explain how to add one)"}
499
+ `);
500
+ });
501
+ }
502
+
503
+ // src/profile.ts
504
+ var import_fs3 = require("fs");
120
505
  var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
121
506
  var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
122
507
  var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
@@ -198,13 +583,13 @@ function decodeStrict(bytes) {
198
583
  function listEntries(dir, shown) {
199
584
  const entries = [];
200
585
  if (process.platform === "win32") {
201
- for (const d of (0, import_fs.readdirSync)(dir, { withFileTypes: true })) {
586
+ for (const d of (0, import_fs3.readdirSync)(dir, { withFileTypes: true })) {
202
587
  const problem = nameProblem(d.name);
203
588
  if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
204
589
  entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
205
590
  }
206
591
  } else {
207
- for (const d of (0, import_fs.readdirSync)(dir, { withFileTypes: true, encoding: "buffer" })) {
592
+ for (const d of (0, import_fs3.readdirSync)(dir, { withFileTypes: true, encoding: "buffer" })) {
208
593
  let name;
209
594
  try {
210
595
  name = decodeStrict(d.name);
@@ -221,23 +606,23 @@ var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or dire
221
606
  function readRegularFile(full, rel, budget) {
222
607
  let fd;
223
608
  try {
224
- fd = (0, import_fs.openSync)(full, import_fs.constants.O_RDONLY | (import_fs.constants.O_NOFOLLOW ?? 0));
609
+ fd = (0, import_fs3.openSync)(full, import_fs3.constants.O_RDONLY | (import_fs3.constants.O_NOFOLLOW ?? 0));
225
610
  } catch (e) {
226
611
  const code = e.code;
227
612
  if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
228
613
  throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
229
614
  }
230
615
  try {
231
- const st = (0, import_fs.fstatSync)(fd);
616
+ const st = (0, import_fs3.fstatSync)(fd);
232
617
  if (!st.isFile()) throw new PackageError(specialMessage(rel));
233
618
  if (st.size > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} is ${st.size} bytes, over the CLI's local read guard of ${LOCAL_LIMITS.maxFileBytes} bytes.`);
234
- const bytes = (0, import_fs.readFileSync)(fd);
619
+ const bytes = (0, import_fs3.readFileSync)(fd);
235
620
  if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
236
621
  budget.total += bytes.length;
237
622
  if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
238
623
  return bytes;
239
624
  } finally {
240
- (0, import_fs.closeSync)(fd);
625
+ (0, import_fs3.closeSync)(fd);
241
626
  }
242
627
  }
243
628
  var extensionOf = (name) => {
@@ -247,7 +632,7 @@ var extensionOf = (name) => {
247
632
  function buildPackage(root) {
248
633
  let rootStat;
249
634
  try {
250
- rootStat = (0, import_fs.lstatSync)(root);
635
+ rootStat = (0, import_fs3.lstatSync)(root);
251
636
  } catch {
252
637
  throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
253
638
  }
@@ -256,7 +641,7 @@ function buildPackage(root) {
256
641
  const files = [];
257
642
  const skipped = [];
258
643
  const budget = { total: 0 };
259
- const walk = (dir, prefix, depth) => {
644
+ const walk2 = (dir, prefix, depth) => {
260
645
  if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
261
646
  for (const entry of listEntries(dir, prefix || ".")) {
262
647
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -268,7 +653,7 @@ function buildPackage(root) {
268
653
  skipped.push({ path: rel, reason: "vendored directory" });
269
654
  continue;
270
655
  }
271
- walk(full, rel, depth + 1);
656
+ walk2(full, rel, depth + 1);
272
657
  continue;
273
658
  }
274
659
  if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
@@ -286,7 +671,7 @@ function buildPackage(root) {
286
671
  files.push({ path: rel, content });
287
672
  }
288
673
  };
289
- walk(root, "", 0);
674
+ walk2(root, "", 0);
290
675
  if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
291
676
  return { files, skipped };
292
677
  }
@@ -299,7 +684,7 @@ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
299
684
  try {
300
685
  const response = await fetch(apiUrl, {
301
686
  method: "POST",
302
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/1.6.0" },
687
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/2.0.0" },
303
688
  body,
304
689
  signal: controller.signal
305
690
  });
@@ -365,7 +750,7 @@ function formatAssessment(body, skipped) {
365
750
  }
366
751
 
367
752
  // src/cli.ts
368
- var VERSION = "1.6.0";
753
+ var VERSION = "2.0.0";
369
754
  var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
370
755
  var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
371
756
  var EXIT_OK = 0;
@@ -387,18 +772,18 @@ var write = (value = "") => process.stdout.write(`${value}
387
772
  `);
388
773
  var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
389
774
  var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
390
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
775
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
391
776
  function collectFiles(dir) {
392
777
  const files = [];
393
778
  try {
394
- const entries = (0, import_fs2.readdirSync)(dir);
779
+ const entries = (0, import_fs4.readdirSync)(dir);
395
780
  for (const entry of entries) {
396
- if (SKIP_DIRS.has(entry)) continue;
397
- const fullPath = (0, import_path.join)(dir, entry);
398
- const stat = (0, import_fs2.statSync)(fullPath);
781
+ if (SKIP_DIRS2.has(entry)) continue;
782
+ const fullPath = (0, import_path3.join)(dir, entry);
783
+ const stat = (0, import_fs4.statSync)(fullPath);
399
784
  if (stat.isDirectory()) {
400
785
  files.push(...collectFiles(fullPath));
401
- } else if (SUPPORTED_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) {
786
+ } else if (SUPPORTED_EXTENSIONS.has((0, import_path3.extname)(entry).toLowerCase())) {
402
787
  files.push(fullPath);
403
788
  }
404
789
  }
@@ -458,9 +843,12 @@ ${c.gray}Execution trust analysis for AI agents${c.reset}
458
843
  ${c.bold}Usage:${c.reset}
459
844
  agentverify scan [dir] --key <api-key>
460
845
  agentverify scan --file <file> --key <api-key>
846
+ agentverify mcp
461
847
 
462
848
  ${c.bold}Commands:${c.reset}
463
849
  scan Scan agent files in a directory or a single file
850
+ mcp Run as an MCP server so Claude Code, Cursor, VS Code and other AI
851
+ tools can scan agents while you code (reads AGENTVERIFY_API_KEY)
464
852
 
465
853
  ${c.bold}Options:${c.reset}
466
854
  --key, -k API key (or set AGENTVERIFY_API_KEY env var)
@@ -498,6 +886,9 @@ ${c.bold}Examples:${c.reset}
498
886
  ${c.gray}# Markdown summary for pull requests${c.reset}
499
887
  agentverify scan . --key av_your_key --markdown
500
888
 
889
+ ${c.gray}# Let Claude Code scan your agents (one-time setup)${c.reset}
890
+ claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp
891
+
501
892
  ${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
502
893
  agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
503
894
 
@@ -578,8 +969,8 @@ ${c.bold}Threat categories:${c.reset}`);
578
969
  write(`${c.gray}Re-scan:${c.reset} agentverify scan --file ${fileName} --key $AGENTVERIFY_API_KEY
579
970
  `);
580
971
  }
581
- function markdownSummary(results, summary) {
582
- const lines = ["# Agent Verify", "", `Files scanned: ${summary.total}`, `Verified: ${summary.verified}`, `Not verified: ${summary.notVerified}`, `Not assessed: ${summary.notAssessed}`, `Errors: ${summary.errors}`, ""];
972
+ function markdownSummary(results, summary2) {
973
+ const lines = ["# Agent Verify", "", `Files scanned: ${summary2.total}`, `Verified: ${summary2.verified}`, `Not verified: ${summary2.notVerified}`, `Not assessed: ${summary2.notAssessed}`, `Errors: ${summary2.errors}`, ""];
583
974
  for (const item of results) {
584
975
  if (!item.result) {
585
976
  lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
@@ -599,20 +990,20 @@ function markdownSummary(results, summary) {
599
990
  lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
600
991
  return lines.join("\n");
601
992
  }
602
- function writeGithubJobSummary(results, summary) {
993
+ function writeGithubJobSummary(results, summary2) {
603
994
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
604
995
  if (!summaryPath) return;
605
996
  try {
606
- (0, import_fs2.appendFileSync)(summaryPath, `${markdownSummary(results, summary)}
997
+ (0, import_fs4.appendFileSync)(summaryPath, `${markdownSummary(results, summary2)}
607
998
  `);
608
999
  } catch (e) {
609
1000
  console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
610
1001
  }
611
1002
  }
612
- function writeSummaryFile(path, results, summary) {
1003
+ function writeSummaryFile(path, results, summary2) {
613
1004
  if (!path) return;
614
1005
  try {
615
- (0, import_fs2.writeFileSync)(path, JSON.stringify({ results, summary }, null, 2));
1006
+ (0, import_fs4.writeFileSync)(path, JSON.stringify({ results, summary: summary2 }, null, 2));
616
1007
  } catch (e) {
617
1008
  console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
618
1009
  }
@@ -670,12 +1061,12 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
670
1061
  const batch = files.slice(i, i + CONCURRENCY);
671
1062
  const batchResults = await Promise.allSettled(
672
1063
  batch.map(async (filePath) => {
673
- const fileStat = (0, import_fs2.statSync)(filePath);
1064
+ const fileStat = (0, import_fs4.statSync)(filePath);
674
1065
  if (fileStat.size > MAX_SCAN_FILE_BYTES) {
675
1066
  throw new Error(`File is ${(fileStat.size / 1024 / 1024).toFixed(1)}MB, over the ${MAX_SCAN_FILE_BYTES / 1024 / 1024}MB scan limit \u2014 skipped`);
676
1067
  }
677
- const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
678
- const relPath = (0, import_path.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
1068
+ const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
1069
+ const relPath = (0, import_path3.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
679
1070
  const result = await scan(
680
1071
  { content, fileName: relPath, policyId: args.policy || void 0 },
681
1072
  { apiKey: args.key, apiUrl: API_URL }
@@ -697,7 +1088,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
697
1088
  else notVerified++;
698
1089
  } else {
699
1090
  const filePath = batch[j];
700
- const relPath = (0, import_path.relative)(args.dir, filePath);
1091
+ const relPath = (0, import_path3.relative)(args.dir, filePath);
701
1092
  const errMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
702
1093
  if (!args.json) {
703
1094
  write(` ${c.yellow}\u26A0${c.reset} ${c.yellow}WARNING${c.reset} ${relPath.padEnd(30)} ${c.gray}${errMsg}${c.reset}`);
@@ -707,11 +1098,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
707
1098
  }
708
1099
  }
709
1100
  }
710
- const summary = { total: files.length, verified, notVerified, notAssessed, errors };
711
- writeGithubJobSummary(jsonResults, summary);
712
- writeSummaryFile(args.summaryFile, jsonResults, summary);
1101
+ const summary2 = { total: files.length, verified, notVerified, notAssessed, errors };
1102
+ writeGithubJobSummary(jsonResults, summary2);
1103
+ writeSummaryFile(args.summaryFile, jsonResults, summary2);
713
1104
  if (args.json) {
714
- write(JSON.stringify({ results: jsonResults, summary }, null, 2));
1105
+ write(JSON.stringify({ results: jsonResults, summary: summary2 }, null, 2));
715
1106
  if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
716
1107
  if (args.ci) {
717
1108
  if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
@@ -720,7 +1111,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
720
1111
  return;
721
1112
  }
722
1113
  if (args.markdown) {
723
- write(markdownSummary(jsonResults, summary));
1114
+ write(markdownSummary(jsonResults, summary2));
724
1115
  if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
725
1116
  if (args.ci) {
726
1117
  if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
@@ -843,6 +1234,10 @@ async function main() {
843
1234
  printHelp();
844
1235
  process.exit(EXIT_OK);
845
1236
  }
1237
+ if (args.command === "mcp") {
1238
+ await runMcpServer({ version: VERSION, apiKey: args.key, apiUrl: API_URL });
1239
+ return;
1240
+ }
846
1241
  if (args.command === "scan") {
847
1242
  if (args.profiles.length > 0) await runProfile(args);
848
1243
  else await runScan(args);