agentaudit 3.1.0 → 3.3.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.
Files changed (4) hide show
  1. package/README.md +122 -105
  2. package/cli.mjs +258 -9
  3. package/index.mjs +193 -72
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -4,10 +4,11 @@
4
4
 
5
5
  **Security scanner for AI packages**
6
6
 
7
- Scan MCP servers, agent skills, and AI tools for vulnerabilities — from the terminal or via MCP.
7
+ Scan MCP servers, agent skills, and AI tools for vulnerabilities.
8
+ MCP server for agents + standalone CLI for humans.
8
9
 
9
10
  [![npm](https://img.shields.io/npm/v/agentaudit?style=flat-square&color=00C853)](https://www.npmjs.com/package/agentaudit)
10
- [![Trust Registry](https://img.shields.io/badge/Registry-agentaudit.dev-00C853?style=flat-square)](https://agentaudit.dev)
11
+ [![Registry](https://img.shields.io/badge/Registry-agentaudit.dev-00C853?style=flat-square)](https://agentaudit.dev)
11
12
  [![License](https://img.shields.io/badge/License-AGPL_3.0-F9A825?style=flat-square)](LICENSE)
12
13
 
13
14
  </div>
@@ -17,76 +18,100 @@ Scan MCP servers, agent skills, and AI tools for vulnerabilities — from the te
17
18
  ## Quick Start
18
19
 
19
20
  ```bash
20
- # Install globally
21
- npm install -g agentaudit
21
+ npx agentaudit discover
22
+ ```
23
+
24
+ That's it. Finds all MCP servers on your machine and checks them against the security registry.
25
+
26
+ ```
27
+ AgentAudit v3.2.0
28
+ Security scanner for AI packages
22
29
 
23
- # Setup (register + get API key — free, one-time)
24
- agentaudit setup
30
+ • Scanning Claude Desktop ~/.claude/mcp.json found 2 servers
25
31
 
26
- # Scan a repo
27
- agentaudit scan https://github.com/owner/repo
32
+ ├── fastmcp-demo npm:fastmcp
33
+ │ SAFE Risk 0 ✔ official https://agentaudit.dev/skills/fastmcp
34
+ └── my-tool npm:some-mcp-tool
35
+ ⚠ not audited Run: agentaudit audit <source-url>
28
36
 
29
- # Scan multiple repos
30
- agentaudit scan repo1 repo2 repo3
37
+ ────────────────────────────────────────────────────────
38
+ Summary 2 servers across 1 config
31
39
 
32
- # Check if a package has been audited
33
- agentaudit check fastmcp
40
+ ✔ 1 audited
41
+ ⚠ 1 not audited
34
42
  ```
35
43
 
36
- Or run without installing:
44
+ ## Install
37
45
 
38
46
  ```bash
39
- npx agentaudit scan https://github.com/owner/repo
47
+ npm install -g agentaudit # global install
48
+ # or use directly:
49
+ npx agentaudit <command>
40
50
  ```
41
51
 
42
- ## What it does
52
+ ---
43
53
 
44
- ```
45
- ◉ google-workspace-mcp https://github.com/taylorwilsdon/google_workspace_mcp
46
- │ Python mcp-server 31 files scanned in 1.0s
47
-
48
- ├── tool drive_service ✔ ok
49
- ├── tool docs_service ✔ ok
50
- ├── tool start_google_auth ✔ ok
51
- └── tool set_enabled_tools ✔ ok
52
-
53
- │ Findings (2) static analysis — may include false positives
54
- ├── ● MEDIUM Potential hardcoded secret
55
- │ .env.oauth21:9 SECRET="your-google-client-secret"
56
- └── ● MEDIUM Potential path traversal
57
- auth/credential_store.py:123
58
-
59
- └── registry LOW Risk 10 https://agentaudit.dev/skills/google-workspace-mcp
60
- ```
54
+ ## Commands
61
55
 
62
- **Detects:**
63
- - 🔴 Prompt injection & tool poisoning
64
- - 🔴 Shell command injection
65
- - 🔴 SQL injection
66
- - 🟡 Hardcoded secrets
67
- - 🟡 SSL/TLS verification disabled
68
- - 🟡 Path traversal
69
- - 🟡 Unsafe YAML/pickle deserialization
70
- - 🔵 Wildcard CORS
71
- - 🔵 Undisclosed telemetry
56
+ | Command | What it does | Speed |
57
+ |---------|-------------|-------|
58
+ | `discover` | Find local MCP servers + check registry | ⚡ instant |
59
+ | `check <name>` | Look up a package in the registry | ⚡ instant |
60
+ | `scan <url>` | Quick static analysis (regex-based, local) | 🔵 ~2s |
61
+ | `audit <url>` | **Deep LLM-powered security audit** | 🔴 ~30s |
62
+ | `setup` | Register + configure API key | interactive |
63
+
64
+ ### `scan` vs `audit`
65
+
66
+ | | `scan` | `audit` |
67
+ |--|--------|---------|
68
+ | **How** | Regex pattern matching | LLM 3-pass: UNDERSTAND → DETECT → CLASSIFY |
69
+ | **Speed** | ~2 seconds | ~30 seconds |
70
+ | **Depth** | Surface-level patterns | Semantic code understanding |
71
+ | **Needs LLM API key** | No | Yes (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) |
72
+ | **Uploads to registry** | No | Yes (with `agentaudit setup`) |
73
+
74
+ ### Examples
75
+
76
+ ```bash
77
+ # Discover all MCP servers on your machine
78
+ npx agentaudit discover
79
+
80
+ # Quick static scan
81
+ npx agentaudit scan https://github.com/owner/repo
82
+
83
+ # Deep LLM-powered audit
84
+ export ANTHROPIC_API_KEY=sk-ant-...
85
+ npx agentaudit audit https://github.com/owner/repo
86
+
87
+ # Export code + audit prompt for manual LLM review
88
+ npx agentaudit audit https://github.com/owner/repo --export
72
89
 
73
- **Plus** registry lookup — shows if a package has already been officially audited on [agentaudit.dev](https://agentaudit.dev).
90
+ # Registry lookup
91
+ npx agentaudit check fastmcp
92
+
93
+ # Register for an API key (free)
94
+ npx agentaudit setup
95
+ ```
74
96
 
75
97
  ---
76
98
 
77
99
  ## MCP Server
78
100
 
79
- Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP client. Your AI agent gets three tools:
101
+ Add AgentAudit to your AI editor. Your agent gets 4 tools:
80
102
 
81
- | Tool | Description |
82
- |------|-------------|
83
- | `audit_package` | Clone a repo, return source code + audit prompt for deep LLM analysis |
84
- | `submit_report` | Upload completed audit report to [agentaudit.dev](https://agentaudit.dev) |
85
- | `check_package` | Look up a package in the registry |
103
+ | MCP Tool | Description |
104
+ |----------|-------------|
105
+ | `discover_servers` | Find all locally installed MCP servers, check registry status |
106
+ | `audit_package` | Clone a repo → return source code + audit prompt you analyze → `submit_report` |
107
+ | `submit_report` | Upload your audit report to [agentaudit.dev](https://agentaudit.dev) |
108
+ | `check_package` | Quick registry lookup for a package |
86
109
 
87
- ### Claude Desktop / Claude Code
110
+ ### Setup
88
111
 
89
- `~/.claude/mcp.json`:
112
+ One-line config — works with `npx`, no manual clone needed:
113
+
114
+ **Claude Desktop** (`~/.claude/mcp.json`):
90
115
  ```json
91
116
  {
92
117
  "mcpServers": {
@@ -98,9 +123,7 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
98
123
  }
99
124
  ```
100
125
 
101
- ### Cursor
102
-
103
- `.cursor/mcp.json`:
126
+ **Cursor** (`.cursor/mcp.json`):
104
127
  ```json
105
128
  {
106
129
  "mcpServers": {
@@ -112,9 +135,7 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
112
135
  }
113
136
  ```
114
137
 
115
- ### Windsurf
116
-
117
- `~/.codeium/windsurf/mcp_config.json`:
138
+ **Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
118
139
  ```json
119
140
  {
120
141
  "mcpServers": {
@@ -126,81 +147,77 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
126
147
  }
127
148
  ```
128
149
 
129
- > **That's it.** No manual clone, no path config. `npx` handles everything.
130
-
131
- ### How the MCP audit works
150
+ ### How an audit works via MCP
132
151
 
133
152
  ```
134
- Agent calls audit_package("https://github.com/owner/repo")
135
-
136
- MCP Server clones repo, collects source files (max 300KB)
137
-
138
- Returns source code + 3-pass audit methodology
139
-
140
- Agent's LLM analyzes code (UNDERSTAND → DETECT → CLASSIFY)
141
-
142
- Agent calls submit_report(findings)
143
-
144
- Report published at agentaudit.dev/skills/{slug}
153
+ You: "Audit the blender-mcp server"
154
+
155
+ Agent calls discover_servers finds blender-mcp
156
+
157
+ Agent calls check_package("blender-mcp") not in registry
158
+
159
+ Agent calls audit_package("https://github.com/user/blender-mcp")
160
+
161
+ MCP server clones repo, returns source code + audit methodology
162
+
163
+ Agent's LLM analyzes code (3-pass: UNDERSTAND → DETECT → CLASSIFY)
164
+
165
+ Agent calls submit_report(findings_json)
166
+
167
+ Report published at agentaudit.dev/skills/blender-mcp
145
168
  ```
146
169
 
147
- ---
170
+ ### Authentication
148
171
 
149
- ## Setup & Authentication
172
+ Run `npx agentaudit setup` once. Both CLI and MCP server find credentials automatically:
150
173
 
151
- ```bash
152
- agentaudit setup
153
- ```
154
-
155
- Interactive wizard — choose:
156
- 1. **Register new agent** (free) → API key created automatically
157
- 2. **Enter existing API key** → if you already have one
158
-
159
- Credentials are stored in `~/.config/agentaudit/credentials.json` (survives reinstalls).
160
-
161
- The MCP server finds credentials automatically from:
162
174
  1. `AGENTAUDIT_API_KEY` environment variable
163
- 2. `~/.config/agentaudit/credentials.json`
175
+ 2. `~/.config/agentaudit/credentials.json` (created by `setup`)
164
176
 
165
- **Scanning and checking work without a key.** Only submitting reports requires authentication.
177
+ **`discover`, `scan`, and `check` work without a key.** Only `audit`/`submit_report` need one.
166
178
 
167
179
  ---
168
180
 
169
- ## CLI Reference
181
+ ## What it detects
170
182
 
171
- ```
172
- agentaudit setup Register + configure API key
173
- agentaudit scan <url> [url...] Scan Git repositories
174
- agentaudit check <name> Look up package in registry
175
- agentaudit --help Show help
176
- ```
183
+ ### Static scan (`scan`)
177
184
 
178
- ### Examples
185
+ - 🔴 Prompt injection & tool poisoning
186
+ - 🔴 Shell command injection
187
+ - 🔴 SQL injection
188
+ - 🔴 Unsafe deserialization (pickle, YAML)
189
+ - 🟡 Hardcoded secrets
190
+ - 🟡 SSL/TLS verification disabled
191
+ - 🟡 Path traversal
192
+ - 🔵 Wildcard CORS
193
+ - 🔵 Undisclosed telemetry
179
194
 
180
- ```bash
181
- # Scan a single repo
182
- agentaudit scan https://github.com/jlowin/fastmcp
195
+ ### Deep audit (`audit` / MCP `audit_package`)
183
196
 
184
- # Scan multiple repos at once
185
- agentaudit scan https://github.com/owner/repo1 https://github.com/owner/repo2
197
+ Everything above, plus:
186
198
 
187
- # Check registry for existing audit
188
- agentaudit check mongodb-mcp-server
189
- ```
199
+ - 🔴 Multi-file attack chains (credential harvest → exfiltration)
200
+ - 🔴 Agent manipulation (impersonation, capability escalation, jailbreaks)
201
+ - 🔴 MCP-specific: tool description injection, resource traversal, unpinned npx
202
+ - 🟡 Persistence mechanisms (crontab, shell RC, git hooks, systemd)
203
+ - 🟡 Obfuscation (base64 exec, zero-width chars, ANSI escapes)
204
+ - 🟡 Context pollution & indirect prompt injection
205
+
206
+ 50+ detection patterns across 8 categories. [Full pattern list →](https://github.com/starbuck100/agentaudit-skill)
190
207
 
191
208
  ---
192
209
 
193
210
  ## Requirements
194
211
 
195
212
  - **Node.js 18+**
196
- - **Git** (for cloning repos during scan)
213
+ - **Git** (for cloning repos)
197
214
 
198
215
  ---
199
216
 
200
217
  ## Related
201
218
 
202
- - [agentaudit.dev](https://agentaudit.dev) — Trust registry & audit reports
203
- - [agentaudit-skill](https://github.com/starbuck100/agentaudit-skill) — Full agent skill with gate scripts, detection patterns & peer review
219
+ - **[agentaudit.dev](https://agentaudit.dev)** — Trust registry with 400+ audit reports
220
+ - **[agentaudit-skill](https://github.com/starbuck100/agentaudit-skill)** — Full agent skill with gate scripts, detection patterns & peer review system
204
221
 
205
222
  ---
206
223
 
package/cli.mjs CHANGED
@@ -780,7 +780,7 @@ async function discoverCommand() {
780
780
  } else {
781
781
  unauditedServers++;
782
782
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
783
- console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: agentaudit scan <source-url>${c.reset}`);
783
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: agentaudit audit <source-url>${c.reset}`);
784
784
  }
785
785
 
786
786
  if (server.sourceUrl) {
@@ -806,6 +806,236 @@ async function discoverCommand() {
806
806
  }
807
807
  }
808
808
 
809
+ // ── Audit command (deep LLM-powered) ────────────────────
810
+
811
+ function loadAuditPrompt() {
812
+ const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
813
+ if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
814
+ return null;
815
+ }
816
+
817
+ async function auditRepo(url) {
818
+ const start = Date.now();
819
+ const slug = slugFromUrl(url);
820
+
821
+ console.log(`${icons.scan} ${c.bold}Auditing ${slug}${c.reset} ${c.dim}${url}${c.reset}`);
822
+ console.log(`${icons.pipe} ${c.dim}Deep LLM-powered analysis (3-pass: UNDERSTAND → DETECT → CLASSIFY)${c.reset}`);
823
+ console.log();
824
+
825
+ // Step 1: Clone
826
+ process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
827
+ const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
828
+ const repoPath = path.join(tmpDir, 'repo');
829
+ try {
830
+ execSync(`git clone --depth 1 "${url}" "${repoPath}" 2>/dev/null`, {
831
+ timeout: 30_000, stdio: 'pipe',
832
+ });
833
+ console.log(` ${c.green}done${c.reset}`);
834
+ } catch {
835
+ console.log(` ${c.red}failed${c.reset}`);
836
+ return null;
837
+ }
838
+
839
+ // Step 2: Collect files
840
+ process.stdout.write(` ${c.dim}[2/4]${c.reset} Collecting source files...`);
841
+ const files = collectFiles(repoPath);
842
+ console.log(` ${c.green}${files.length} files${c.reset}`);
843
+
844
+ // Step 3: Build audit payload
845
+ process.stdout.write(` ${c.dim}[3/4]${c.reset} Preparing audit payload...`);
846
+ const auditPrompt = loadAuditPrompt();
847
+
848
+ let codeBlock = '';
849
+ for (const file of files) {
850
+ codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
851
+ }
852
+ console.log(` ${c.green}done${c.reset}`);
853
+
854
+ // Step 4: LLM Analysis
855
+ // Check for API keys to determine which LLM to use
856
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
857
+ const openaiKey = process.env.OPENAI_API_KEY;
858
+
859
+ if (!anthropicKey && !openaiKey) {
860
+ // No LLM API key — output the prepared audit for piping or MCP use
861
+ console.log();
862
+ console.log(` ${c.yellow}No LLM API key found.${c.reset} To run the audit automatically, set one of:`);
863
+ console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
864
+ console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
865
+ console.log();
866
+ console.log(` ${c.bold}Alternatives:${c.reset}`);
867
+ console.log(` ${c.dim}1.${c.reset} Use the MCP server in Claude/Cursor — your agent runs the audit automatically`);
868
+ console.log(` ${c.dim}2.${c.reset} Export for manual review: ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
869
+ console.log();
870
+
871
+ // Check if --export flag
872
+ if (process.argv.includes('--export')) {
873
+ const exportPath = path.join(process.cwd(), `audit-${slug}.md`);
874
+ const exportContent = [
875
+ `# Security Audit: ${slug}`,
876
+ `**Source:** ${url}`,
877
+ `**Files:** ${files.length}`,
878
+ ``,
879
+ `## Audit Instructions`,
880
+ ``,
881
+ auditPrompt || '(audit prompt not found)',
882
+ ``,
883
+ `## Report Format`,
884
+ ``,
885
+ `After analysis, produce a JSON report:`,
886
+ '```json',
887
+ `{ "skill_slug": "${slug}", "source_url": "${url}", "risk_score": 0, "result": "safe", "findings": [] }`,
888
+ '```',
889
+ ``,
890
+ `## Source Code`,
891
+ ``,
892
+ codeBlock,
893
+ ].join('\n');
894
+ fs.writeFileSync(exportPath, exportContent);
895
+ console.log(` ${icons.safe} Exported to ${c.bold}${exportPath}${c.reset}`);
896
+ console.log(` ${c.dim}Paste this into any LLM (Claude, ChatGPT, etc.) for analysis${c.reset}`);
897
+ }
898
+
899
+ // Cleanup
900
+ try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
901
+ return null;
902
+ }
903
+
904
+ // We have an API key — run LLM audit
905
+ process.stdout.write(` ${c.dim}[4/4]${c.reset} Running LLM analysis...`);
906
+
907
+ const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
908
+ const userMessage = [
909
+ `Audit this package: **${slug}** (${url})`,
910
+ ``,
911
+ `After analysis, respond with ONLY a JSON object (no markdown, no explanation):`,
912
+ '```',
913
+ `{ "skill_slug": "${slug}", "source_url": "${url}", "package_type": "<mcp-server|agent-skill|library|cli-tool>",`,
914
+ ` "risk_score": <0-100>, "result": "<safe|caution|unsafe>", "max_severity": "<none|low|medium|high|critical>",`,
915
+ ` "findings_count": <n>, "findings": [{ "id": "...", "title": "...", "severity": "...", "category": "...",`,
916
+ ` "description": "...", "file": "...", "line": <n>, "remediation": "...", "confidence": "...", "is_by_design": false }] }`,
917
+ '```',
918
+ ``,
919
+ `## Source Code`,
920
+ codeBlock,
921
+ ].join('\n');
922
+
923
+ let report = null;
924
+
925
+ try {
926
+ if (anthropicKey) {
927
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
928
+ method: 'POST',
929
+ headers: {
930
+ 'x-api-key': anthropicKey,
931
+ 'anthropic-version': '2023-06-01',
932
+ 'content-type': 'application/json',
933
+ },
934
+ body: JSON.stringify({
935
+ model: 'claude-sonnet-4-20250514',
936
+ max_tokens: 8192,
937
+ system: systemPrompt,
938
+ messages: [{ role: 'user', content: userMessage }],
939
+ }),
940
+ signal: AbortSignal.timeout(120_000),
941
+ });
942
+ const data = await res.json();
943
+ const text = data.content?.[0]?.text || '';
944
+ // Extract JSON from response
945
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
946
+ if (jsonMatch) report = JSON.parse(jsonMatch[0]);
947
+ } else if (openaiKey) {
948
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
949
+ method: 'POST',
950
+ headers: {
951
+ 'Authorization': `Bearer ${openaiKey}`,
952
+ 'Content-Type': 'application/json',
953
+ },
954
+ body: JSON.stringify({
955
+ model: 'gpt-4o',
956
+ max_tokens: 8192,
957
+ messages: [
958
+ { role: 'system', content: systemPrompt },
959
+ { role: 'user', content: userMessage },
960
+ ],
961
+ }),
962
+ signal: AbortSignal.timeout(120_000),
963
+ });
964
+ const data = await res.json();
965
+ const text = data.choices?.[0]?.message?.content || '';
966
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
967
+ if (jsonMatch) report = JSON.parse(jsonMatch[0]);
968
+ }
969
+
970
+ console.log(` ${c.green}done${c.reset} ${c.dim}(${elapsed(start)})${c.reset}`);
971
+ } catch (err) {
972
+ console.log(` ${c.red}failed${c.reset}`);
973
+ console.log(` ${c.red}${err.message}${c.reset}`);
974
+ try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
975
+ return null;
976
+ }
977
+
978
+ // Cleanup repo
979
+ try { execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' }); } catch {}
980
+
981
+ if (!report) {
982
+ console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
983
+ return null;
984
+ }
985
+
986
+ // Display results
987
+ console.log();
988
+ const riskScore = report.risk_score || 0;
989
+ console.log(` ${riskBadge(riskScore)} Risk ${riskScore}/100 ${c.bold}${report.result || 'unknown'}${c.reset}`);
990
+ console.log();
991
+
992
+ if (report.findings && report.findings.length > 0) {
993
+ console.log(` ${c.bold}Findings (${report.findings.length})${c.reset}`);
994
+ console.log();
995
+ for (const f of report.findings) {
996
+ const sc = severityColor(f.severity);
997
+ console.log(` ${severityIcon(f.severity)} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${f.title}`);
998
+ if (f.file) console.log(` ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
999
+ if (f.description) console.log(` ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
1000
+ console.log();
1001
+ }
1002
+ } else {
1003
+ console.log(` ${c.green}No findings — package looks clean.${c.reset}`);
1004
+ console.log();
1005
+ }
1006
+
1007
+ // Upload to registry
1008
+ const creds = loadCredentials();
1009
+ if (creds) {
1010
+ process.stdout.write(` Uploading report to registry...`);
1011
+ try {
1012
+ const res = await fetch(`${REGISTRY_URL}/api/reports`, {
1013
+ method: 'POST',
1014
+ headers: {
1015
+ 'Authorization': `Bearer ${creds.api_key}`,
1016
+ 'Content-Type': 'application/json',
1017
+ },
1018
+ body: JSON.stringify(report),
1019
+ signal: AbortSignal.timeout(15_000),
1020
+ });
1021
+ if (res.ok) {
1022
+ const data = await res.json();
1023
+ console.log(` ${c.green}done${c.reset}`);
1024
+ console.log(` ${c.dim}Report: ${REGISTRY_URL}/skills/${slug}${c.reset}`);
1025
+ } else {
1026
+ console.log(` ${c.yellow}failed (HTTP ${res.status})${c.reset}`);
1027
+ }
1028
+ } catch (err) {
1029
+ console.log(` ${c.yellow}failed${c.reset}`);
1030
+ }
1031
+ } else {
1032
+ console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to upload reports to the registry${c.reset}`);
1033
+ }
1034
+
1035
+ console.log();
1036
+ return report;
1037
+ }
1038
+
809
1039
  // ── Check command ───────────────────────────────────────
810
1040
 
811
1041
  async function checkPackage(name) {
@@ -815,7 +1045,7 @@ async function checkPackage(name) {
815
1045
  const data = await checkRegistry(name);
816
1046
  if (!data) {
817
1047
  console.log(` ${c.yellow}Not found${c.reset} — package "${name}" hasn't been audited yet.`);
818
- console.log(` ${c.dim}Run: agentaudit scan <repo-url> to audit it${c.reset}`);
1048
+ console.log(` ${c.dim}Run: agentaudit audit <repo-url> for a deep LLM audit${c.reset}`);
819
1049
  return;
820
1050
  }
821
1051
 
@@ -835,17 +1065,22 @@ async function main() {
835
1065
 
836
1066
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
837
1067
  banner();
838
- console.log(` ${c.bold}Usage:${c.reset}`);
839
- console.log(` agentaudit setup Register + configure API key`);
840
- console.log(` agentaudit discover Find & check local MCP servers`);
841
- console.log(` agentaudit scan <repo-url> [repo-url...] Scan repositories`);
842
- console.log(` agentaudit check <package-name> Look up in registry`);
1068
+ console.log(` ${c.bold}Commands:${c.reset}`);
1069
+ console.log();
1070
+ console.log(` ${c.cyan}agentaudit discover${c.reset} Find local MCP servers + check registry`);
1071
+ console.log(` ${c.cyan}agentaudit scan${c.reset} <url> [url...] Quick static scan (regex, local)`);
1072
+ console.log(` ${c.cyan}agentaudit audit${c.reset} <url> [url...] Deep LLM-powered security audit`);
1073
+ console.log(` ${c.cyan}agentaudit check${c.reset} <name> Look up package in registry`);
1074
+ console.log(` ${c.cyan}agentaudit setup${c.reset} Register + configure API key`);
1075
+ console.log();
1076
+ console.log(` ${c.bold}scan${c.reset} vs ${c.bold}audit${c.reset}:`);
1077
+ console.log(` ${c.dim}scan = fast regex-based static analysis (~2s)${c.reset}`);
1078
+ console.log(` ${c.dim}audit = deep LLM analysis with 3-pass methodology (~30s)${c.reset}`);
843
1079
  console.log();
844
1080
  console.log(` ${c.bold}Examples:${c.reset}`);
845
- console.log(` agentaudit setup`);
846
1081
  console.log(` agentaudit discover`);
847
1082
  console.log(` agentaudit scan https://github.com/owner/repo`);
848
- console.log(` agentaudit scan repo1.git repo2.git repo3.git`);
1083
+ console.log(` agentaudit audit https://github.com/owner/repo`);
849
1084
  console.log(` agentaudit check fastmcp`);
850
1085
  console.log();
851
1086
  process.exit(0);
@@ -879,6 +1114,7 @@ async function main() {
879
1114
  if (targets.length === 0) {
880
1115
  console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
881
1116
  console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit discover${c.dim} to find & check locally installed MCP servers${c.reset}`);
1117
+ console.log(` ${c.dim}Tip: use ${c.cyan}agentaudit audit <url>${c.dim} for a deep LLM-powered audit${c.reset}`);
882
1118
  process.exit(1);
883
1119
  }
884
1120
 
@@ -894,6 +1130,19 @@ async function main() {
894
1130
  return;
895
1131
  }
896
1132
 
1133
+ if (command === 'audit') {
1134
+ const urls = targets.filter(t => !t.startsWith('--'));
1135
+ if (urls.length === 0) {
1136
+ console.log(` ${c.red}Error: at least one repository URL required${c.reset}`);
1137
+ process.exit(1);
1138
+ }
1139
+
1140
+ for (const url of urls) {
1141
+ await auditRepo(url);
1142
+ }
1143
+ return;
1144
+ }
1145
+
897
1146
  console.log(` ${c.red}Unknown command: ${command}${c.reset}`);
898
1147
  console.log(` ${c.dim}Run agentaudit --help for usage${c.reset}`);
899
1148
  process.exit(1);
package/index.mjs CHANGED
@@ -2,25 +2,20 @@
2
2
  /**
3
3
  * AgentAudit MCP Server
4
4
  *
5
- * Provides security audit capabilities via Model Context Protocol.
5
+ * Security audit capabilities via Model Context Protocol.
6
6
  *
7
7
  * Tools:
8
- * - audit_package: Clone a repo, read source files, return with audit prompt
9
- * - submit_report: Upload a completed audit report to agentaudit.dev
10
- * - check_package: Look up a package in the AgentAudit registry
8
+ * - discover_servers Find locally installed MCP servers + check registry status
9
+ * - audit_package Clone a repo, return source code + audit prompt for LLM analysis
10
+ * - submit_report Upload a completed audit report to agentaudit.dev
11
+ * - check_package Look up a package in the AgentAudit registry
11
12
  *
12
13
  * Usage:
13
- * node mcp-server/index.mjs
14
+ * npx agentaudit (starts MCP server via stdio)
15
+ * node index.mjs (same)
14
16
  *
15
- * Configure in Claude/Cursor/etc:
16
- * {
17
- * "mcpServers": {
18
- * "agentaudit": {
19
- * "command": "node",
20
- * "args": ["path/to/agentaudit/mcp-server/index.mjs"]
21
- * }
22
- * }
23
- * }
17
+ * Configure in Claude/Cursor/Windsurf:
18
+ * { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }
24
19
  */
25
20
 
26
21
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -37,8 +32,8 @@ import { fileURLToPath } from 'url';
37
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
38
33
  const SKILL_DIR = path.resolve(__dirname);
39
34
  const REGISTRY_URL = 'https://agentaudit.dev';
40
- const MAX_FILE_SIZE = 50_000; // 50KB per file
41
- const MAX_TOTAL_SIZE = 300_000; // 300KB total code
35
+ const MAX_FILE_SIZE = 50_000;
36
+ const MAX_TOTAL_SIZE = 300_000;
42
37
  const SKIP_DIRS = new Set([
43
38
  'node_modules', '.git', '__pycache__', '.venv', 'venv', 'dist', 'build',
44
39
  '.next', '.nuxt', 'coverage', '.pytest_cache', '.mypy_cache', 'vendor',
@@ -61,7 +56,7 @@ const PRIORITY_FILES = [
61
56
  'Makefile', 'Dockerfile', 'docker-compose.yml',
62
57
  ];
63
58
 
64
- // ── Helpers ──────────────────────────────────────────────
59
+ // ── Credentials ─────────────────────────────────────────
65
60
 
66
61
  function loadApiKey() {
67
62
  if (process.env.AGENTAUDIT_API_KEY) return process.env.AGENTAUDIT_API_KEY;
@@ -84,39 +79,32 @@ function loadApiKey() {
84
79
 
85
80
  function loadAuditPrompt() {
86
81
  const promptPath = path.join(SKILL_DIR, 'prompts', 'audit-prompt.md');
87
- if (fs.existsSync(promptPath)) {
88
- return fs.readFileSync(promptPath, 'utf8');
89
- }
82
+ if (fs.existsSync(promptPath)) return fs.readFileSync(promptPath, 'utf8');
90
83
  return 'ERROR: audit-prompt.md not found at ' + promptPath;
91
84
  }
92
85
 
86
+ // ── File Collection ─────────────────────────────────────
87
+
93
88
  function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0 }) {
94
89
  if (totalSize.bytes >= MAX_TOTAL_SIZE) return collected;
95
-
96
90
  let entries;
97
91
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
98
92
  catch { return collected; }
99
-
100
- // Sort: priority files first
101
93
  entries.sort((a, b) => {
102
94
  const aP = PRIORITY_FILES.includes(a.name) ? 0 : 1;
103
95
  const bP = PRIORITY_FILES.includes(b.name) ? 0 : 1;
104
96
  return aP - bP || a.name.localeCompare(b.name);
105
97
  });
106
-
107
98
  for (const entry of entries) {
108
99
  if (totalSize.bytes >= MAX_TOTAL_SIZE) break;
109
-
110
100
  const relPath = basePath ? `${basePath}/${entry.name}` : entry.name;
111
101
  const fullPath = path.join(dir, entry.name);
112
-
113
102
  if (entry.isDirectory()) {
114
103
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
115
104
  collectFiles(fullPath, relPath, collected, totalSize);
116
105
  } else {
117
106
  const ext = path.extname(entry.name).toLowerCase();
118
107
  if (SKIP_EXTENSIONS.has(ext)) continue;
119
-
120
108
  try {
121
109
  const stat = fs.statSync(fullPath);
122
110
  if (stat.size > MAX_FILE_SIZE) {
@@ -124,24 +112,22 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
124
112
  continue;
125
113
  }
126
114
  if (stat.size === 0) continue;
127
-
128
115
  const content = fs.readFileSync(fullPath, 'utf8');
129
116
  totalSize.bytes += content.length;
130
117
  collected.push({ path: relPath, content });
131
- } catch {
132
- // Binary or unreadable — skip
133
- }
118
+ } catch {}
134
119
  }
135
120
  }
136
121
  return collected;
137
122
  }
138
123
 
124
+ // ── Repo Helpers ────────────────────────────────────────
125
+
139
126
  function cloneRepo(sourceUrl) {
140
127
  const tmpDir = fs.mkdtempSync('/tmp/agentaudit-');
141
128
  try {
142
129
  execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo" 2>/dev/null`, {
143
- timeout: 30_000,
144
- stdio: 'pipe',
130
+ timeout: 30_000, stdio: 'pipe',
145
131
  });
146
132
  return path.join(tmpDir, 'repo');
147
133
  } catch (err) {
@@ -150,30 +136,95 @@ function cloneRepo(sourceUrl) {
150
136
  }
151
137
 
152
138
  function cleanupRepo(repoPath) {
153
- try {
154
- execSync(`rm -rf "${path.dirname(repoPath)}"`, { stdio: 'pipe' });
155
- } catch {}
139
+ try { execSync(`rm -rf "${path.dirname(repoPath)}"`, { stdio: 'pipe' }); } catch {}
156
140
  }
157
141
 
158
142
  function slugFromUrl(url) {
159
- // https://github.com/owner/repo → owner-repo or just repo
160
143
  const match = url.match(/github\.com\/([^/]+)\/([^/.\s]+)/);
161
144
  if (match) return match[2].toLowerCase().replace(/[^a-z0-9-]/g, '-');
162
145
  return url.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 60);
163
146
  }
164
147
 
148
+ // ── Discover local MCP configs ──────────────────────────
149
+
150
+ function discoverMcpServers() {
151
+ const home = process.env.HOME || process.env.USERPROFILE || '';
152
+ const candidates = [
153
+ { platform: 'Claude Desktop', path: path.join(home, '.claude', 'mcp.json') },
154
+ { platform: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
155
+ { platform: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json') },
156
+ { platform: 'Claude Desktop', path: path.join(home, '.config', 'claude', 'claude_desktop_config.json') },
157
+ { platform: 'Cursor', path: path.join(home, '.cursor', 'mcp.json') },
158
+ { platform: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json') },
159
+ { platform: 'VS Code', path: path.join(home, '.vscode', 'mcp.json') },
160
+ ];
161
+
162
+ const results = [];
163
+
164
+ for (const c of candidates) {
165
+ if (!fs.existsSync(c.path)) {
166
+ results.push({ platform: c.platform, config_path: c.path, status: 'not found', servers: [] });
167
+ continue;
168
+ }
169
+ let content;
170
+ try { content = JSON.parse(fs.readFileSync(c.path, 'utf8')); }
171
+ catch { results.push({ platform: c.platform, config_path: c.path, status: 'parse error', servers: [] }); continue; }
172
+
173
+ const serverMap = content.mcpServers || content.servers || {};
174
+ const servers = [];
175
+ for (const [name, cfg] of Object.entries(serverMap)) {
176
+ const allArgs = [cfg.command, ...(cfg.args || [])].filter(Boolean).join(' ');
177
+ const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
178
+ const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
179
+ servers.push({
180
+ name,
181
+ command: cfg.command || null,
182
+ args: cfg.args || [],
183
+ npm_package: npxMatch?.[1] || null,
184
+ pip_package: pyMatch?.[1] || null,
185
+ });
186
+ }
187
+ results.push({ platform: c.platform, config_path: c.path, status: 'found', server_count: servers.length, servers });
188
+ }
189
+
190
+ return results;
191
+ }
192
+
193
+ async function checkRegistry(slug) {
194
+ try {
195
+ const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
196
+ signal: AbortSignal.timeout(5000),
197
+ });
198
+ if (res.ok) return await res.json();
199
+ } catch {}
200
+ return null;
201
+ }
202
+
165
203
  // ── MCP Server ───────────────────────────────────────────
166
204
 
167
205
  const server = new Server(
168
- { name: 'agentaudit', version: '1.0.0' },
206
+ { name: 'agentaudit', version: '3.2.0' },
169
207
  { capabilities: { tools: {} } }
170
208
  );
171
209
 
172
210
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
173
211
  tools: [
212
+ {
213
+ name: 'discover_servers',
214
+ description: 'Find all locally installed MCP servers by scanning config files (Claude Desktop, Cursor, Windsurf, VS Code). Returns the list of configured servers with their names, commands, and package sources. Use this to see what MCP servers are installed, then check each against the registry with check_package, or audit them with audit_package.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ check_registry: {
219
+ type: 'boolean',
220
+ description: 'If true, also check each discovered server against the AgentAudit registry (default: true)',
221
+ },
222
+ },
223
+ },
224
+ },
174
225
  {
175
226
  name: 'audit_package',
176
- description: 'Clone a repository and prepare it for security audit. Returns the source code and audit instructions. You (the agent) then analyze the code following the audit prompt and call submit_report with the results.',
227
+ description: 'Deep security audit: clone a repository and prepare it for LLM-powered analysis. Returns the source code and a 3-pass audit methodology (UNDERSTAND → DETECT → CLASSIFY). You (the agent) then analyze the code following the instructions and call submit_report with your findings. This is a DEEP audit — use check_package first for a quick registry lookup.',
177
228
  inputSchema: {
178
229
  type: 'object',
179
230
  properties: {
@@ -187,13 +238,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
187
238
  },
188
239
  {
189
240
  name: 'submit_report',
190
- description: 'Submit a completed security audit report to the AgentAudit registry (agentaudit.dev). Call this after you have analyzed the code from audit_package.',
241
+ description: 'Submit a completed security audit report to the AgentAudit registry (agentaudit.dev). Call this after you have analyzed the code from audit_package. The report becomes publicly available and helps other agents make install decisions.',
191
242
  inputSchema: {
192
243
  type: 'object',
193
244
  properties: {
194
245
  report: {
195
246
  type: 'object',
196
- description: 'The audit report JSON object. Must include: skill_slug, source_url, risk_score (0-100), result (safe|caution|unsafe), findings (array), findings_count, max_severity.',
247
+ description: 'The audit report JSON object. Required fields: skill_slug, source_url, risk_score (0-100), result (safe|caution|unsafe), findings (array), findings_count, max_severity, package_type.',
197
248
  },
198
249
  },
199
250
  required: ['report'],
@@ -201,7 +252,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
201
252
  },
202
253
  {
203
254
  name: 'check_package',
204
- description: 'Look up a package in the AgentAudit security registry. Returns the latest audit results if available.',
255
+ description: 'Quick registry lookup: check if a package has already been audited on agentaudit.dev. Returns the latest audit results (risk score, findings, official status) if available. Use this before audit_package to avoid duplicate work.',
205
256
  inputSchema: {
206
257
  type: 'object',
207
258
  properties: {
@@ -218,39 +269,99 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
218
269
 
219
270
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
220
271
  const { name, arguments: args } = request.params;
221
-
272
+
222
273
  switch (name) {
274
+
275
+ // ── discover_servers ──────────────────────────────────
276
+ case 'discover_servers': {
277
+ const doRegistryCheck = args.check_registry !== false;
278
+ const configs = discoverMcpServers();
279
+ const foundConfigs = configs.filter(c => c.status === 'found');
280
+ const allServers = foundConfigs.flatMap(c => c.servers.map(s => ({ ...s, platform: c.platform })));
281
+
282
+ let text = `# Discovered MCP Servers\n\n`;
283
+ text += `Scanned ${configs.length} config locations. Found ${foundConfigs.length} config(s) with ${allServers.length} server(s).\n\n`;
284
+
285
+ for (const config of configs) {
286
+ if (config.status === 'not found') continue;
287
+ text += `## ${config.platform}\n`;
288
+ text += `Config: \`${config.config_path}\`\n\n`;
289
+
290
+ if (config.servers.length === 0) {
291
+ text += `No servers configured.\n\n`;
292
+ continue;
293
+ }
294
+
295
+ for (const srv of config.servers) {
296
+ const slug = srv.npm_package?.replace(/^@/, '').replace(/\//g, '-')
297
+ || srv.pip_package?.replace(/[^a-z0-9-]/gi, '-')
298
+ || srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
299
+
300
+ text += `### ${srv.name}\n`;
301
+ text += `- Command: \`${[srv.command, ...srv.args].join(' ')}\`\n`;
302
+ if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
303
+ if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
304
+
305
+ if (doRegistryCheck) {
306
+ const regData = await checkRegistry(slug);
307
+ if (regData) {
308
+ const risk = regData.risk_score ?? regData.latest_risk_score ?? 0;
309
+ const official = regData.has_official_audit ? ' (official)' : '';
310
+ text += `- **Registry: ✅ Audited** — Risk ${risk}/100${official}\n`;
311
+ text += `- Report: ${REGISTRY_URL}/skills/${slug}\n`;
312
+ } else {
313
+ text += `- **Registry: ⚠️ Not audited** — no audit report found\n`;
314
+ text += `- To audit: call \`audit_package\` with the source URL\n`;
315
+ }
316
+ }
317
+ text += `\n`;
318
+ }
319
+ }
320
+
321
+ if (allServers.length === 0) {
322
+ text += `No MCP servers found. Config locations searched:\n`;
323
+ text += `- Claude Desktop: ~/.claude/mcp.json\n`;
324
+ text += `- Cursor: ~/.cursor/mcp.json\n`;
325
+ text += `- Windsurf: ~/.codeium/windsurf/mcp_config.json\n`;
326
+ text += `- VS Code: ~/.vscode/mcp.json\n`;
327
+ }
328
+
329
+ return { content: [{ type: 'text', text }] };
330
+ }
331
+
332
+ // ── audit_package ─────────────────────────────────────
223
333
  case 'audit_package': {
224
334
  const { source_url } = args;
225
335
  if (!source_url || !source_url.startsWith('http')) {
226
336
  return { content: [{ type: 'text', text: 'Error: source_url must be a valid HTTP(S) URL' }] };
227
337
  }
228
-
338
+
229
339
  let repoPath;
230
340
  try {
231
341
  repoPath = cloneRepo(source_url);
232
342
  const files = collectFiles(repoPath);
233
343
  const slug = slugFromUrl(source_url);
234
344
  const auditPrompt = loadAuditPrompt();
235
-
236
- // Build the response
345
+
237
346
  let codeBlock = '';
238
347
  for (const file of files) {
239
348
  codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
240
349
  }
241
-
350
+
242
351
  const response = [
243
- `# Security Audit Request`,
352
+ `# Security Audit: ${slug}`,
244
353
  ``,
245
- `**Package:** ${slug}`,
246
354
  `**Source:** ${source_url}`,
247
355
  `**Files collected:** ${files.length}`,
248
356
  ``,
249
- `## Instructions`,
357
+ `## Your Task`,
358
+ ``,
359
+ `1. Analyze the source code below using the 3-pass audit methodology`,
360
+ `2. Call \`submit_report\` with your findings as JSON`,
250
361
  ``,
251
- `Analyze the source code below following the audit methodology. After your analysis, call the \`submit_report\` tool with your findings as a JSON object.`,
362
+ `## Report Format`,
252
363
  ``,
253
- `The report JSON must include:`,
364
+ `Your report JSON must include:`,
254
365
  '```json',
255
366
  `{`,
256
367
  ` "skill_slug": "${slug}",`,
@@ -285,7 +396,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
285
396
  ``,
286
397
  codeBlock,
287
398
  ].join('\n');
288
-
399
+
289
400
  return { content: [{ type: 'text', text: response }] };
290
401
  } catch (err) {
291
402
  return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
@@ -293,27 +404,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
293
404
  if (repoPath) cleanupRepo(repoPath);
294
405
  }
295
406
  }
296
-
407
+
408
+ // ── submit_report ─────────────────────────────────────
297
409
  case 'submit_report': {
298
410
  const { report } = args;
299
411
  if (!report || typeof report !== 'object') {
300
412
  return { content: [{ type: 'text', text: 'Error: report must be a JSON object' }] };
301
413
  }
302
-
414
+
303
415
  const apiKey = loadApiKey();
304
416
  if (!apiKey) {
305
- return { content: [{ type: 'text', text: 'Error: No API key configured. Set AGENTAUDIT_API_KEY or register first.' }] };
417
+ return { content: [{ type: 'text', text: 'Error: No API key configured. Run `npx agentaudit setup` or set AGENTAUDIT_API_KEY.' }] };
306
418
  }
307
-
308
- // Validate required fields
419
+
309
420
  const required = ['skill_slug', 'source_url', 'risk_score', 'result'];
310
421
  for (const field of required) {
311
422
  if (report[field] == null) {
312
423
  return { content: [{ type: 'text', text: `Error: Missing required field "${field}" in report` }] };
313
424
  }
314
425
  }
315
-
316
- // Auto-fix findings
426
+
317
427
  if (!Array.isArray(report.findings)) report.findings = [];
318
428
  report.findings_count = report.findings.length;
319
429
  if (!report.max_severity) {
@@ -324,7 +434,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
324
434
  return fi < mi ? f.severity : max;
325
435
  }, 'none');
326
436
  }
327
-
437
+
328
438
  try {
329
439
  const res = await fetch(`${REGISTRY_URL}/api/reports`, {
330
440
  method: 'POST',
@@ -335,13 +445,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
335
445
  body: JSON.stringify(report),
336
446
  signal: AbortSignal.timeout(60_000),
337
447
  });
338
-
448
+
339
449
  const body = await res.text();
340
450
  let data;
341
451
  try { data = JSON.parse(body); } catch { data = { raw: body }; }
342
-
452
+
343
453
  if (res.ok) {
344
- return { content: [{ type: 'text', text: `Report submitted successfully!\nReport ID: ${data.report_id || 'unknown'}\nURL: ${REGISTRY_URL}/skills/${report.skill_slug}\n\n${JSON.stringify(data, null, 2)}` }] };
454
+ return { content: [{ type: 'text', text: `✅ Report submitted!\n\nReport ID: ${data.report_id || 'unknown'}\nURL: ${REGISTRY_URL}/skills/${report.skill_slug}\nRisk: ${report.risk_score}/100 (${report.result})\nFindings: ${report.findings_count}` }] };
345
455
  } else {
346
456
  return { content: [{ type: 'text', text: `Upload failed (HTTP ${res.status}): ${JSON.stringify(data, null, 2)}` }] };
347
457
  }
@@ -349,31 +459,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
349
459
  return { content: [{ type: 'text', text: `Upload error: ${err.message}` }] };
350
460
  }
351
461
  }
352
-
462
+
463
+ // ── check_package ─────────────────────────────────────
353
464
  case 'check_package': {
354
465
  const { package_name } = args;
355
466
  if (!package_name) {
356
467
  return { content: [{ type: 'text', text: 'Error: package_name is required' }] };
357
468
  }
358
-
469
+
359
470
  try {
360
471
  const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(package_name)}`, {
361
472
  signal: AbortSignal.timeout(10_000),
362
473
  });
363
-
474
+
364
475
  if (res.status === 404) {
365
- return { content: [{ type: 'text', text: `Package "${package_name}" not found in registry. It may not have been audited yet. Use audit_package to audit it.` }] };
476
+ return { content: [{ type: 'text', text: `Package "${package_name}" not found in registry.\n\nIt hasn't been audited yet. To audit it:\n1. Find the source URL (GitHub repo)\n2. Call audit_package with the URL\n3. Analyze the code\n4. Call submit_report with your findings` }] };
366
477
  }
367
-
478
+
368
479
  const data = await res.json();
369
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
480
+ const risk = data.risk_score ?? data.latest_risk_score ?? 'unknown';
481
+ const official = data.has_official_audit ? '✅ Officially audited' : 'Community audit';
482
+
483
+ let summary = `# ${package_name}\n\n`;
484
+ summary += `**Risk Score:** ${risk}/100\n`;
485
+ summary += `**Status:** ${official}\n`;
486
+ if (data.source_url) summary += `**Source:** ${data.source_url}\n`;
487
+ summary += `**Registry:** ${REGISTRY_URL}/skills/${package_name}\n\n`;
488
+ summary += `## Full Data\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
489
+
490
+ return { content: [{ type: 'text', text: summary }] };
370
491
  } catch (err) {
371
492
  return { content: [{ type: 'text', text: `Registry lookup failed: ${err.message}` }] };
372
493
  }
373
494
  }
374
-
495
+
375
496
  default:
376
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
497
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}. Available: discover_servers, audit_package, submit_report, check_package` }] };
377
498
  }
378
499
  });
379
500
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {