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/dist/cli.mjs CHANGED
@@ -4,11 +4,396 @@ import {
4
4
  } from "./chunk-XVA2M2HS.mjs";
5
5
 
6
6
  // src/cli.ts
7
- import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync, appendFileSync, writeFileSync } from "fs";
8
- import { join, relative, extname } from "path";
7
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2, appendFileSync, writeFileSync } from "fs";
8
+ import { join as join2, relative as relative2, extname as extname2 } from "path";
9
+
10
+ // src/mcp.ts
11
+ import { createInterface } from "readline";
12
+ import { readFileSync as readFileSync2 } from "fs";
13
+ import { resolve } from "path";
14
+
15
+ // src/files.ts
16
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
17
+ import { basename, extname, join, relative, sep } from "path";
18
+ import { homedir, platform } from "os";
19
+ var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
20
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache", ".venv", "venv", "__pycache__", "out", "target"]);
21
+ var SKIP_FILES = /* @__PURE__ */ new Set(["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Cargo.lock", "composer.lock", "tsconfig.json"]);
22
+ var MAX_FILES = 60;
23
+ var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
24
+ var MAX_FILE_BYTES = 400 * 1024;
25
+ function filePriority(relPath) {
26
+ const p = relPath.toLowerCase().replace(/\\/g, "/");
27
+ const name = p.split("/").pop() ?? p;
28
+ let score = 0;
29
+ if (/(^|\/)(tests?|__tests__|spec|e2e|fixtures?|examples?)\//.test(p) || /\.(test|spec)\.[a-z]+$/.test(name)) score -= 80;
30
+ if (/(agent|tool|mcp|prompt|crew|graph|server|config|policy|skill)/.test(name)) score += 30;
31
+ if (/(^|\/)(agents?|tools?|mcp|src|app|lib)\//.test(p)) score += 10;
32
+ if (name.endsWith(".md")) score -= 20;
33
+ if (name === "skill.md" || name === "agents.md" || name === "claude.md") score += 40;
34
+ return score - p.split("/").length;
35
+ }
36
+ function walk(dir, out) {
37
+ let entries;
38
+ try {
39
+ entries = readdirSync(dir);
40
+ } catch {
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ if (SKIP_DIRS.has(entry) || SKIP_FILES.has(entry)) continue;
45
+ const full = join(dir, entry);
46
+ let st;
47
+ try {
48
+ st = statSync(full);
49
+ } catch {
50
+ continue;
51
+ }
52
+ if (st.isDirectory()) walk(full, out);
53
+ else if (SCANNABLE_EXTENSIONS.has(extname(entry).toLowerCase())) out.push(full);
54
+ }
55
+ }
56
+ function pickFiles(target) {
57
+ const notes = [];
58
+ let st;
59
+ try {
60
+ st = statSync(target);
61
+ } catch {
62
+ throw new Error(`Path not found: ${target}`);
63
+ }
64
+ if (st.isFile()) {
65
+ if (st.size > MAX_TOTAL_BYTES) throw new Error(`${basename(target)} is larger than 5MB, which is the most one scan can take.`);
66
+ return { files: [{ path: basename(target), content: readFileSync(target, "utf8") }], notes };
67
+ }
68
+ const all = [];
69
+ walk(target, all);
70
+ if (all.length === 0) throw new Error(`No agent files found in ${target}. Agent Verify reads ${[...SCANNABLE_EXTENSIONS].join(" ")} files.`);
71
+ const ranked = all.map((full) => ({ full, rel: relative(target, full).split(sep).join("/") })).sort((a, b) => filePriority(b.rel) - filePriority(a.rel) || a.rel.localeCompare(b.rel));
72
+ const files = [];
73
+ let total = 0;
74
+ let tooBig = 0;
75
+ for (const { full, rel } of ranked) {
76
+ if (files.length >= MAX_FILES) break;
77
+ let size = 0;
78
+ try {
79
+ size = statSync(full).size;
80
+ } catch {
81
+ continue;
82
+ }
83
+ if (size > MAX_FILE_BYTES) {
84
+ tooBig++;
85
+ continue;
86
+ }
87
+ if (total + size > MAX_TOTAL_BYTES) break;
88
+ files.push({ path: rel, content: readFileSync(full, "utf8") });
89
+ total += size;
90
+ }
91
+ const left = ranked.length - files.length - tooBig;
92
+ if (left > 0) notes.push(`${left} lower-priority file(s) were left out to stay within one scan (${MAX_FILES} files / 5MB).`);
93
+ if (tooBig > 0) notes.push(`${tooBig} file(s) over 400KB were skipped.`);
94
+ return { files, notes };
95
+ }
96
+ function combineFiles(files) {
97
+ if (files.length === 1) return files[0].content;
98
+ return files.map((f) => `// ===== FILE: ${f.path} =====
99
+ ${f.content}`).join("\n\n");
100
+ }
101
+ function locateLine(files, combinedLine) {
102
+ if (files.length === 1) return { path: files[0].path, line: combinedLine };
103
+ let cursor = 1;
104
+ for (let i = 0; i < files.length; i++) {
105
+ const start = cursor + 1;
106
+ const lines = files[i].content.split("\n").length;
107
+ if (combinedLine >= start && combinedLine < start + lines) return { path: files[i].path, line: combinedLine - start + 1 };
108
+ cursor = start + lines + 1;
109
+ }
110
+ return null;
111
+ }
112
+ function knownMcpConfigPaths(cwd = process.cwd()) {
113
+ const home = homedir();
114
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
115
+ const claudeDesktop = platform() === "win32" ? join(appData, "Claude", "claude_desktop_config.json") : platform() === "darwin" ? join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join(home, ".config", "Claude", "claude_desktop_config.json");
116
+ return [
117
+ { client: "Claude Code (this project)", path: join(cwd, ".mcp.json") },
118
+ { client: "Claude Code (user)", path: join(home, ".claude.json") },
119
+ { client: "Claude Desktop", path: claudeDesktop },
120
+ { client: "Cursor (this project)", path: join(cwd, ".cursor", "mcp.json") },
121
+ { client: "Cursor (user)", path: join(home, ".cursor", "mcp.json") },
122
+ { client: "VS Code (this project)", path: join(cwd, ".vscode", "mcp.json") },
123
+ { client: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") }
124
+ ];
125
+ }
126
+ function extractMcpServers(raw) {
127
+ let data;
128
+ try {
129
+ data = JSON.parse(raw);
130
+ } catch {
131
+ return null;
132
+ }
133
+ if (!data || typeof data !== "object") return null;
134
+ const servers = {};
135
+ const take = (obj, prefix = "") => {
136
+ if (!obj || typeof obj !== "object") return;
137
+ for (const key of ["mcpServers", "servers"]) {
138
+ const block = obj[key];
139
+ if (block && typeof block === "object" && !Array.isArray(block)) {
140
+ for (const [name, def] of Object.entries(block)) servers[prefix + name] = def;
141
+ }
142
+ }
143
+ };
144
+ take(data);
145
+ const projects = data.projects;
146
+ if (projects && typeof projects === "object") {
147
+ for (const [proj, value] of Object.entries(projects)) take(value, `${basename(proj)}/`);
148
+ }
149
+ return Object.keys(servers).length ? servers : null;
150
+ }
151
+ var SECRET_NAME = /(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PAT|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
152
+ var SECRET_VALUE = /^(sk-|sk_|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|npm_|glpat-|eyJ)/;
153
+ function mask(value) {
154
+ return value.length <= 4 ? "****redacted" : `${value.slice(0, 4)}****redacted`;
155
+ }
156
+ function redactMcpSecrets(servers) {
157
+ const clean = (node, parentKey = "") => {
158
+ if (typeof node === "string") {
159
+ if (/^\$\{|^<|^\$[A-Z_]/.test(node)) return node;
160
+ if (SECRET_NAME.test(parentKey) || SECRET_VALUE.test(node)) return mask(node);
161
+ const flag = node.match(/^(--?[A-Za-z0-9_-]*(?:token|key|secret|password|auth)[A-Za-z0-9_-]*=)(.+)$/i);
162
+ if (flag) return flag[1] + mask(flag[2]);
163
+ return node;
164
+ }
165
+ if (Array.isArray(node)) return node.map((item) => clean(item, parentKey));
166
+ if (node && typeof node === "object") {
167
+ const out = {};
168
+ for (const [k, v] of Object.entries(node)) out[k] = clean(v, k);
169
+ return out;
170
+ }
171
+ return node;
172
+ };
173
+ return clean(servers);
174
+ }
175
+ function existingMcpConfigs(cwd = process.cwd()) {
176
+ return knownMcpConfigPaths(cwd).filter((c2) => existsSync(c2.path));
177
+ }
178
+
179
+ // src/mcp.ts
180
+ var SUPPORTED_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
181
+ var PRICING_URL = "https://aimodularity.com/agentverify/pricing/";
182
+ var DASHBOARD_URL = "https://aimodularity.com/agentverify/dashboard/";
183
+ var TOOLS = [
184
+ {
185
+ name: "scan_path",
186
+ title: "Scan agent files",
187
+ 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.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: { path: { type: "string", description: "File or folder to scan. Relative paths are resolved from the current project." } },
191
+ required: ["path"],
192
+ additionalProperties: false
193
+ }
194
+ },
195
+ {
196
+ name: "scan_code",
197
+ title: "Scan agent code",
198
+ 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.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ content: { type: "string", description: "The code or config to scan." },
203
+ fileName: { type: "string", description: "A file name with the right extension (e.g. agent.py, mcp.json) so the language is detected." }
204
+ },
205
+ required: ["content"],
206
+ additionalProperties: false
207
+ }
208
+ },
209
+ {
210
+ name: "scan_mcp_config",
211
+ title: "Check my MCP servers",
212
+ 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.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: { path: { type: "string", description: "Optional: a specific MCP config file. Leave empty to check all known locations." } },
216
+ additionalProperties: false
217
+ }
218
+ }
219
+ ];
220
+ var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
221
+ var SEVERITY_LABEL = { critical: "CRITICAL", high: "HIGH", medium: "MEDIUM", low: "LOW" };
222
+ function verdictLine(result) {
223
+ const verdict = result.verdict === "VERIFIED" ? "VERIFIED" : result.verdict === "NOT_ASSESSED" ? "NOT ASSESSED" : "NOT VERIFIED";
224
+ return `**Verdict: ${verdict}** \u2014 Security score ${result.riskScore}/100 (higher is safer, ${result.riskLevel})`;
225
+ }
226
+ function fixText(f) {
227
+ if (f.fixLocked) return `Fix: included with Agent Verify Pro (${PRICING_URL})`;
228
+ const steps = (f.recommendedFix ?? "").trim();
229
+ const code = (f.quickFix ?? f.fixCode ?? "").trim();
230
+ if (!steps && !code) return null;
231
+ return [steps ? `Fix: ${steps}` : "", code ? "```\n" + code + "\n```" : ""].filter(Boolean).join("\n");
232
+ }
233
+ function formatReport(result, opts) {
234
+ const findings = [...result.findings ?? []].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
235
+ const counts = ["critical", "high", "medium", "low"].map((s) => [s, findings.filter((f) => f.severity === s).length]).filter(([, n]) => n > 0);
236
+ const lines = [`# Agent Verify: ${opts.title}`, "", verdictLine(result)];
237
+ lines.push(findings.length ? `Findings: ${counts.map(([s, n]) => `${n} ${s}`).join(", ")}` : "No findings.");
238
+ if (result.reportInsights?.topBlocker) lines.push(`Fix first: ${result.reportInsights.topBlocker}`);
239
+ for (const note of opts.notes ?? []) lines.push(`Note: ${note}`);
240
+ findings.forEach((f, i) => {
241
+ const where = typeof f.line === "number" && f.line > 0 ? opts.files ? locateLine(opts.files, f.line) : { path: opts.title, line: f.line } : null;
242
+ lines.push("", `## ${i + 1}. [${SEVERITY_LABEL[f.severity] ?? f.severity}] ${f.title} (${f.code})`);
243
+ if (where) lines.push(`Location: ${where.path}:${where.line}`);
244
+ if (f.evidence) lines.push(`Evidence: ${f.evidence}`);
245
+ lines.push(`What is wrong: ${f.whatIsWrong}`);
246
+ if (f.whyItMatters) lines.push(`Why it matters: ${f.whyItMatters}`);
247
+ const fix = fixText(f);
248
+ if (fix) lines.push(fix);
249
+ });
250
+ const locked = findings.filter((f) => f.fixLocked).length;
251
+ if (locked > 0) lines.push("", `Step-by-step fixes and corrected code for ${locked} finding(s) are included with Pro: ${PRICING_URL}`);
252
+ if (result.reportUrl) lines.push("", `Full report: ${result.reportUrl}`);
253
+ lines.push("", "Agent Verify is an automated, evidence-based check, not a guarantee. Re-scan after making changes.");
254
+ return lines.join("\n");
255
+ }
256
+ function textResult(text, isError = false, structured) {
257
+ return { content: [{ type: "text", text }], ...isError ? { isError: true } : {}, ...structured ? { structuredContent: structured } : {} };
258
+ }
259
+ function summary(result) {
260
+ return {
261
+ verdict: result.verdict,
262
+ securityScore: result.riskScore,
263
+ riskLevel: result.riskLevel,
264
+ findings: (result.findings ?? []).map((f) => ({ code: f.code, severity: f.severity, title: f.title, line: f.line ?? null })),
265
+ reportUrl: result.reportUrl ?? null
266
+ };
267
+ }
268
+ async function callTool(name, args, opts) {
269
+ const tool = TOOLS.find((t) => t.name === name);
270
+ if (!tool) return textResult(`Unknown tool: ${name}`, true);
271
+ if (!opts.apiKey) {
272
+ return textResult(
273
+ "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.",
274
+ true
275
+ );
276
+ }
277
+ const doScan = opts.scanFn ?? scan;
278
+ const cwd = opts.cwd ?? process.cwd();
279
+ try {
280
+ if (name === "scan_code") {
281
+ const content2 = typeof args.content === "string" ? args.content : "";
282
+ if (!content2.trim()) return textResult('Nothing to scan: "content" is empty.', true);
283
+ const fileName = typeof args.fileName === "string" && args.fileName.trim() ? args.fileName.trim() : "agent";
284
+ const result2 = await doScan({ content: content2, fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
285
+ return textResult(formatReport(result2, { title: fileName }), false, summary(result2));
286
+ }
287
+ if (name === "scan_path") {
288
+ const raw = typeof args.path === "string" ? args.path.trim() : "";
289
+ if (!raw) return textResult('Give a file or folder "path" to scan.', true);
290
+ const target = resolve(cwd, raw);
291
+ const picked = pickFiles(target);
292
+ const fileName = picked.files.length === 1 ? picked.files[0].path : `${raw.replace(/[\\/]+$/, "") || "project"} (${picked.files.length} files)`;
293
+ const result2 = await doScan({ content: combineFiles(picked.files), fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
294
+ const notes = [...picked.notes];
295
+ if (picked.files.length > 1) notes.unshift(`Scanned ${picked.files.length} files together as one report.`);
296
+ return textResult(formatReport(result2, { title: fileName, files: picked.files, notes }), false, summary(result2));
297
+ }
298
+ const explicit = typeof args.path === "string" && args.path.trim() ? resolve(cwd, args.path.trim()) : "";
299
+ const configs = explicit ? [{ client: "Config file", path: explicit }] : existingMcpConfigs(cwd);
300
+ 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.');
301
+ const servers = {};
302
+ const read = [];
303
+ for (const cfg of configs) {
304
+ let rawText = "";
305
+ try {
306
+ rawText = readFileSync2(cfg.path, "utf8");
307
+ } catch {
308
+ continue;
309
+ }
310
+ const found = extractMcpServers(rawText);
311
+ if (!found) continue;
312
+ read.push(`${cfg.client}: ${Object.keys(found).length} server(s)`);
313
+ for (const [server, def] of Object.entries(found)) servers[`${cfg.client} \u203A ${server}`] = def;
314
+ }
315
+ if (Object.keys(servers).length === 0) return textResult(`Found ${configs.length} config file(s), but none of them define any MCP servers.`);
316
+ const content = JSON.stringify({ mcpServers: redactMcpSecrets(servers) }, null, 2);
317
+ const result = await doScan({ content, fileName: "mcp.json", platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
318
+ return textResult(
319
+ formatReport(result, { title: `MCP servers (${Object.keys(servers).length})`, notes: [`Checked: ${read.join("; ")}`, "Secret values were masked on this computer before scanning."] }),
320
+ false,
321
+ summary(result)
322
+ );
323
+ } catch (err) {
324
+ return textResult(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
325
+ }
326
+ }
327
+ async function handleMessage(msg, opts) {
328
+ const isNotification = msg.id === void 0 || msg.id === null;
329
+ const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: msg.id, result };
330
+ const fail = (code, message) => isNotification ? null : { jsonrpc: "2.0", id: msg.id ?? null, error: { code, message } };
331
+ switch (msg.method) {
332
+ case "initialize": {
333
+ const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : "";
334
+ return reply({
335
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(requested) ? requested : SUPPORTED_PROTOCOLS[0],
336
+ capabilities: { tools: { listChanged: false } },
337
+ serverInfo: { name: "agentverify", title: "Agent Verify", version: opts.version },
338
+ 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."
339
+ });
340
+ }
341
+ case "ping":
342
+ return reply({});
343
+ case "tools/list":
344
+ return reply({ tools: TOOLS });
345
+ case "tools/call": {
346
+ const name = typeof msg.params?.name === "string" ? msg.params.name : "";
347
+ const args = msg.params?.arguments && typeof msg.params.arguments === "object" ? msg.params.arguments : {};
348
+ return reply(await callTool(name, args, opts));
349
+ }
350
+ case "resources/list":
351
+ return reply({ resources: [] });
352
+ case "prompts/list":
353
+ return reply({ prompts: [] });
354
+ default:
355
+ if (typeof msg.method === "string" && msg.method.startsWith("notifications/")) return null;
356
+ return fail(-32601, `Method not found: ${msg.method}`);
357
+ }
358
+ }
359
+ function runMcpServer(opts) {
360
+ return new Promise((done) => {
361
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
362
+ const send = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
363
+ let pending = 0;
364
+ let closed = false;
365
+ const maybeDone = () => {
366
+ if (closed && pending === 0) done();
367
+ };
368
+ rl.on("line", (line) => {
369
+ const text = line.trim();
370
+ if (!text) return;
371
+ let msg;
372
+ try {
373
+ msg = JSON.parse(text);
374
+ } catch {
375
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
376
+ return;
377
+ }
378
+ pending++;
379
+ handleMessage(msg, opts).then((res) => {
380
+ if (res) send(res);
381
+ }).catch((err) => send({ jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: err instanceof Error ? err.message : "Internal error" } })).finally(() => {
382
+ pending--;
383
+ maybeDone();
384
+ });
385
+ });
386
+ rl.on("close", () => {
387
+ closed = true;
388
+ maybeDone();
389
+ });
390
+ process.stderr.write(`Agent Verify MCP server ${opts.version} ready${opts.apiKey ? "" : " (no API key set: tools will explain how to add one)"}
391
+ `);
392
+ });
393
+ }
9
394
 
10
395
  // src/profile.ts
11
- import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readFileSync } from "fs";
396
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
12
397
  var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
13
398
  var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
14
399
  var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
@@ -90,13 +475,13 @@ function decodeStrict(bytes) {
90
475
  function listEntries(dir, shown) {
91
476
  const entries = [];
92
477
  if (process.platform === "win32") {
93
- for (const d of readdirSync(dir, { withFileTypes: true })) {
478
+ for (const d of readdirSync2(dir, { withFileTypes: true })) {
94
479
  const problem = nameProblem(d.name);
95
480
  if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
96
481
  entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
97
482
  }
98
483
  } else {
99
- for (const d of readdirSync(dir, { withFileTypes: true, encoding: "buffer" })) {
484
+ for (const d of readdirSync2(dir, { withFileTypes: true, encoding: "buffer" })) {
100
485
  let name;
101
486
  try {
102
487
  name = decodeStrict(d.name);
@@ -123,7 +508,7 @@ function readRegularFile(full, rel, budget) {
123
508
  const st = fstatSync(fd);
124
509
  if (!st.isFile()) throw new PackageError(specialMessage(rel));
125
510
  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.`);
126
- const bytes = readFileSync(fd);
511
+ const bytes = readFileSync3(fd);
127
512
  if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
128
513
  budget.total += bytes.length;
129
514
  if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
@@ -148,7 +533,7 @@ function buildPackage(root) {
148
533
  const files = [];
149
534
  const skipped = [];
150
535
  const budget = { total: 0 };
151
- const walk = (dir, prefix, depth) => {
536
+ const walk2 = (dir, prefix, depth) => {
152
537
  if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
153
538
  for (const entry of listEntries(dir, prefix || ".")) {
154
539
  const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -160,7 +545,7 @@ function buildPackage(root) {
160
545
  skipped.push({ path: rel, reason: "vendored directory" });
161
546
  continue;
162
547
  }
163
- walk(full, rel, depth + 1);
548
+ walk2(full, rel, depth + 1);
164
549
  continue;
165
550
  }
166
551
  if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
@@ -178,7 +563,7 @@ function buildPackage(root) {
178
563
  files.push({ path: rel, content });
179
564
  }
180
565
  };
181
- walk(root, "", 0);
566
+ walk2(root, "", 0);
182
567
  if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
183
568
  return { files, skipped };
184
569
  }
@@ -191,7 +576,7 @@ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
191
576
  try {
192
577
  const response = await fetch(apiUrl, {
193
578
  method: "POST",
194
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/1.6.0" },
579
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/2.0.0" },
195
580
  body,
196
581
  signal: controller.signal
197
582
  });
@@ -257,7 +642,7 @@ function formatAssessment(body, skipped) {
257
642
  }
258
643
 
259
644
  // src/cli.ts
260
- var VERSION = "1.6.0";
645
+ var VERSION = "2.0.0";
261
646
  var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
262
647
  var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
263
648
  var EXIT_OK = 0;
@@ -279,18 +664,18 @@ var write = (value = "") => process.stdout.write(`${value}
279
664
  `);
280
665
  var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
281
666
  var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
282
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
667
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
283
668
  function collectFiles(dir) {
284
669
  const files = [];
285
670
  try {
286
- const entries = readdirSync2(dir);
671
+ const entries = readdirSync3(dir);
287
672
  for (const entry of entries) {
288
- if (SKIP_DIRS.has(entry)) continue;
289
- const fullPath = join(dir, entry);
290
- const stat = statSync(fullPath);
673
+ if (SKIP_DIRS2.has(entry)) continue;
674
+ const fullPath = join2(dir, entry);
675
+ const stat = statSync2(fullPath);
291
676
  if (stat.isDirectory()) {
292
677
  files.push(...collectFiles(fullPath));
293
- } else if (SUPPORTED_EXTENSIONS.has(extname(entry).toLowerCase())) {
678
+ } else if (SUPPORTED_EXTENSIONS.has(extname2(entry).toLowerCase())) {
294
679
  files.push(fullPath);
295
680
  }
296
681
  }
@@ -350,9 +735,12 @@ ${c.gray}Execution trust analysis for AI agents${c.reset}
350
735
  ${c.bold}Usage:${c.reset}
351
736
  agentverify scan [dir] --key <api-key>
352
737
  agentverify scan --file <file> --key <api-key>
738
+ agentverify mcp
353
739
 
354
740
  ${c.bold}Commands:${c.reset}
355
741
  scan Scan agent files in a directory or a single file
742
+ mcp Run as an MCP server so Claude Code, Cursor, VS Code and other AI
743
+ tools can scan agents while you code (reads AGENTVERIFY_API_KEY)
356
744
 
357
745
  ${c.bold}Options:${c.reset}
358
746
  --key, -k API key (or set AGENTVERIFY_API_KEY env var)
@@ -390,6 +778,9 @@ ${c.bold}Examples:${c.reset}
390
778
  ${c.gray}# Markdown summary for pull requests${c.reset}
391
779
  agentverify scan . --key av_your_key --markdown
392
780
 
781
+ ${c.gray}# Let Claude Code scan your agents (one-time setup)${c.reset}
782
+ claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp
783
+
393
784
  ${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
394
785
  agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
395
786
 
@@ -470,8 +861,8 @@ ${c.bold}Threat categories:${c.reset}`);
470
861
  write(`${c.gray}Re-scan:${c.reset} agentverify scan --file ${fileName} --key $AGENTVERIFY_API_KEY
471
862
  `);
472
863
  }
473
- function markdownSummary(results, summary) {
474
- const lines = ["# Agent Verify", "", `Files scanned: ${summary.total}`, `Verified: ${summary.verified}`, `Not verified: ${summary.notVerified}`, `Not assessed: ${summary.notAssessed}`, `Errors: ${summary.errors}`, ""];
864
+ function markdownSummary(results, summary2) {
865
+ const lines = ["# Agent Verify", "", `Files scanned: ${summary2.total}`, `Verified: ${summary2.verified}`, `Not verified: ${summary2.notVerified}`, `Not assessed: ${summary2.notAssessed}`, `Errors: ${summary2.errors}`, ""];
475
866
  for (const item of results) {
476
867
  if (!item.result) {
477
868
  lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
@@ -491,20 +882,20 @@ function markdownSummary(results, summary) {
491
882
  lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
492
883
  return lines.join("\n");
493
884
  }
494
- function writeGithubJobSummary(results, summary) {
885
+ function writeGithubJobSummary(results, summary2) {
495
886
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
496
887
  if (!summaryPath) return;
497
888
  try {
498
- appendFileSync(summaryPath, `${markdownSummary(results, summary)}
889
+ appendFileSync(summaryPath, `${markdownSummary(results, summary2)}
499
890
  `);
500
891
  } catch (e) {
501
892
  console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
502
893
  }
503
894
  }
504
- function writeSummaryFile(path, results, summary) {
895
+ function writeSummaryFile(path, results, summary2) {
505
896
  if (!path) return;
506
897
  try {
507
- writeFileSync(path, JSON.stringify({ results, summary }, null, 2));
898
+ writeFileSync(path, JSON.stringify({ results, summary: summary2 }, null, 2));
508
899
  } catch (e) {
509
900
  console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
510
901
  }
@@ -562,12 +953,12 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
562
953
  const batch = files.slice(i, i + CONCURRENCY);
563
954
  const batchResults = await Promise.allSettled(
564
955
  batch.map(async (filePath) => {
565
- const fileStat = statSync(filePath);
956
+ const fileStat = statSync2(filePath);
566
957
  if (fileStat.size > MAX_SCAN_FILE_BYTES) {
567
958
  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`);
568
959
  }
569
- const content = readFileSync2(filePath, "utf-8");
570
- const relPath = relative(args.dir === "." ? process.cwd() : args.dir, filePath);
960
+ const content = readFileSync4(filePath, "utf-8");
961
+ const relPath = relative2(args.dir === "." ? process.cwd() : args.dir, filePath);
571
962
  const result = await scan(
572
963
  { content, fileName: relPath, policyId: args.policy || void 0 },
573
964
  { apiKey: args.key, apiUrl: API_URL }
@@ -589,7 +980,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
589
980
  else notVerified++;
590
981
  } else {
591
982
  const filePath = batch[j];
592
- const relPath = relative(args.dir, filePath);
983
+ const relPath = relative2(args.dir, filePath);
593
984
  const errMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
594
985
  if (!args.json) {
595
986
  write(` ${c.yellow}\u26A0${c.reset} ${c.yellow}WARNING${c.reset} ${relPath.padEnd(30)} ${c.gray}${errMsg}${c.reset}`);
@@ -599,11 +990,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
599
990
  }
600
991
  }
601
992
  }
602
- const summary = { total: files.length, verified, notVerified, notAssessed, errors };
603
- writeGithubJobSummary(jsonResults, summary);
604
- writeSummaryFile(args.summaryFile, jsonResults, summary);
993
+ const summary2 = { total: files.length, verified, notVerified, notAssessed, errors };
994
+ writeGithubJobSummary(jsonResults, summary2);
995
+ writeSummaryFile(args.summaryFile, jsonResults, summary2);
605
996
  if (args.json) {
606
- write(JSON.stringify({ results: jsonResults, summary }, null, 2));
997
+ write(JSON.stringify({ results: jsonResults, summary: summary2 }, null, 2));
607
998
  if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
608
999
  if (args.ci) {
609
1000
  if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
@@ -612,7 +1003,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
612
1003
  return;
613
1004
  }
614
1005
  if (args.markdown) {
615
- write(markdownSummary(jsonResults, summary));
1006
+ write(markdownSummary(jsonResults, summary2));
616
1007
  if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
617
1008
  if (args.ci) {
618
1009
  if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
@@ -735,6 +1126,10 @@ async function main() {
735
1126
  printHelp();
736
1127
  process.exit(EXIT_OK);
737
1128
  }
1129
+ if (args.command === "mcp") {
1130
+ await runMcpServer({ version: VERSION, apiKey: args.key, apiUrl: API_URL });
1131
+ return;
1132
+ }
738
1133
  if (args.command === "scan") {
739
1134
  if (args.profiles.length > 0) await runProfile(args);
740
1135
  else await runScan(args);