agentaudit 3.2.0 → 3.4.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 +152 -124
  2. package/cli.mjs +93 -14
  3. package/index.mjs +234 -72
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -4,89 +4,144 @@
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>
14
15
 
15
16
  ---
16
17
 
17
- ## Quick Start
18
+ ## Getting Started
18
19
 
19
- ```bash
20
- # Install globally
21
- npm install -g agentaudit
22
-
23
- # Setup (register + get API key — free, one-time)
24
- agentaudit setup
20
+ There are two ways to use AgentAudit:
25
21
 
26
- # Scan a repo
27
- agentaudit scan https://github.com/owner/repo
22
+ ### Option A: MCP Server in your AI editor (recommended)
28
23
 
29
- # Scan multiple repos
30
- agentaudit scan repo1 repo2 repo3
24
+ Add AgentAudit to Claude Desktop, Cursor, or Windsurf. **No API key needed** — your editor's agent runs audits using its own LLM.
31
25
 
32
- # Check if a package has been audited
33
- agentaudit check fastmcp
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "agentaudit": {
30
+ "command": "npx",
31
+ "args": ["-y", "agentaudit"]
32
+ }
33
+ }
34
+ }
34
35
  ```
35
36
 
36
- Or run without installing:
37
+ Then just ask your agent: *"Check which MCP servers I have installed and audit any unaudited ones."*
38
+
39
+ ### Option B: CLI
37
40
 
38
41
  ```bash
39
- npx agentaudit scan https://github.com/owner/repo
42
+ # Install
43
+ npm install -g agentaudit # or use npx agentaudit <command>
44
+
45
+ # 1. Discover your MCP servers
46
+ npx agentaudit discover
47
+
48
+ # 2. Audit unaudited packages (needs an LLM API key)
49
+ export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY=sk-...
50
+ npx agentaudit audit https://github.com/owner/repo
51
+
52
+ # 3. (Optional) Register to upload reports to the public registry
53
+ npx agentaudit setup
40
54
  ```
41
55
 
42
- ## What it does
56
+ > **Note:** The `audit` command requires an LLM API key (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) to analyze code. The `discover`, `scan`, and `check` commands work without one. If you don't have an API key, use `--export` to generate a markdown file you can paste into any LLM, or use AgentAudit as an MCP server (Option A) where no extra key is needed.
57
+
58
+ ### Quick example
43
59
 
44
60
  ```
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
61
+ $ npx agentaudit discover
62
+
63
+ AgentAudit v3.3.0
64
+ Security scanner for AI packages
65
+
66
+ Scanning Claude Desktop ~/.claude/mcp.json found 2 servers
67
+
68
+ ├── fastmcp-demo npm:fastmcp
69
+ SAFE Risk 0 official https://agentaudit.dev/skills/fastmcp
70
+ └── my-tool npm:some-mcp-tool
71
+ ⚠ not audited Run: agentaudit audit https://github.com/user/some-mcp-tool
72
+
73
+ Summary 2 servers across 1 config
74
+
75
+ 1 audited
76
+ ⚠ 1 not audited
77
+
78
+ To audit unaudited servers:
79
+ agentaudit audit https://github.com/user/some-mcp-tool (my-tool)
60
80
  ```
61
81
 
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
82
+ ---
72
83
 
73
- **Plus** registry lookup — shows if a package has already been officially audited on [agentaudit.dev](https://agentaudit.dev).
84
+ ## Commands
85
+
86
+ | Command | What it does | Speed |
87
+ |---------|-------------|-------|
88
+ | `discover` | Find local MCP servers + check registry | ⚡ instant |
89
+ | `check <name>` | Look up a package in the registry | ⚡ instant |
90
+ | `scan <url>` | Quick static analysis (regex-based, local) | 🔵 ~2s |
91
+ | `audit <url>` | **Deep LLM-powered security audit** | 🔴 ~30s |
92
+ | `setup` | Register + configure API key | interactive |
93
+
94
+ ### `scan` vs `audit`
95
+
96
+ | | `scan` | `audit` |
97
+ |--|--------|---------|
98
+ | **How** | Regex pattern matching | LLM 3-pass: UNDERSTAND → DETECT → CLASSIFY |
99
+ | **Speed** | ~2 seconds | ~30 seconds |
100
+ | **Depth** | Surface-level patterns | Semantic code understanding |
101
+ | **Needs LLM API key** | No | Yes (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) |
102
+ | **Uploads to registry** | No | Yes (with `agentaudit setup`) |
103
+
104
+ ### Examples
105
+
106
+ ```bash
107
+ # Discover all MCP servers on your machine
108
+ npx agentaudit discover
109
+
110
+ # Quick static scan
111
+ npx agentaudit scan https://github.com/owner/repo
112
+
113
+ # Deep LLM-powered audit
114
+ export ANTHROPIC_API_KEY=sk-ant-...
115
+ npx agentaudit audit https://github.com/owner/repo
116
+
117
+ # Export code + audit prompt for manual LLM review
118
+ npx agentaudit audit https://github.com/owner/repo --export
119
+
120
+ # Registry lookup
121
+ npx agentaudit check fastmcp
122
+
123
+ # Register for an API key (free)
124
+ npx agentaudit setup
125
+ ```
74
126
 
75
127
  ---
76
128
 
77
129
  ## MCP Server
78
130
 
79
- Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP client. Your AI agent gets three tools:
131
+ Add AgentAudit to your AI editor. Your agent gets 4 tools:
80
132
 
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 |
133
+ | MCP Tool | Description |
134
+ |----------|-------------|
135
+ | `discover_servers` | Find all locally installed MCP servers, check registry status |
136
+ | `audit_package` | Clone a repo → return source code + audit prompt you analyze → `submit_report` |
137
+ | `submit_report` | Upload your audit report to [agentaudit.dev](https://agentaudit.dev) |
138
+ | `check_package` | Quick registry lookup for a package |
86
139
 
87
- ### Claude Desktop / Claude Code
140
+ ### Setup
88
141
 
89
- `~/.claude/mcp.json`:
142
+ One-line config — works with `npx`, no manual clone needed:
143
+
144
+ **Claude Desktop** (`~/.claude/mcp.json`):
90
145
  ```json
91
146
  {
92
147
  "mcpServers": {
@@ -98,9 +153,7 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
98
153
  }
99
154
  ```
100
155
 
101
- ### Cursor
102
-
103
- `.cursor/mcp.json`:
156
+ **Cursor** (`.cursor/mcp.json`):
104
157
  ```json
105
158
  {
106
159
  "mcpServers": {
@@ -112,9 +165,7 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
112
165
  }
113
166
  ```
114
167
 
115
- ### Windsurf
116
-
117
- `~/.codeium/windsurf/mcp_config.json`:
168
+ **Windsurf** (`~/.codeium/windsurf/mcp_config.json`):
118
169
  ```json
119
170
  {
120
171
  "mcpServers": {
@@ -126,100 +177,77 @@ Use AgentAudit as an MCP server in Claude Desktop, Cursor, Windsurf, or any MCP
126
177
  }
127
178
  ```
128
179
 
129
- > **That's it.** No manual clone, no path config. `npx` handles everything.
130
-
131
- ### How the MCP audit works
180
+ ### How an audit works via MCP
132
181
 
133
182
  ```
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}
145
- ```
146
-
147
- ---
148
-
149
- ## Setup & Authentication
150
-
151
- ```bash
152
- agentaudit setup
183
+ You: "Audit the blender-mcp server"
184
+
185
+ Agent calls discover_servers finds blender-mcp
186
+
187
+ Agent calls check_package("blender-mcp") not in registry
188
+
189
+ Agent calls audit_package("https://github.com/user/blender-mcp")
190
+
191
+ MCP server clones repo, returns source code + audit methodology
192
+
193
+ Agent's LLM analyzes code (3-pass: UNDERSTAND → DETECT → CLASSIFY)
194
+
195
+ Agent calls submit_report(findings_json)
196
+
197
+ Report published at agentaudit.dev/skills/blender-mcp
153
198
  ```
154
199
 
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
200
+ ### Authentication
158
201
 
159
- Credentials are stored in `~/.config/agentaudit/credentials.json` (survives reinstalls).
202
+ Run `npx agentaudit setup` once. Both CLI and MCP server find credentials automatically:
160
203
 
161
- The MCP server finds credentials automatically from:
162
204
  1. `AGENTAUDIT_API_KEY` environment variable
163
- 2. `~/.config/agentaudit/credentials.json`
205
+ 2. `~/.config/agentaudit/credentials.json` (created by `setup`)
164
206
 
165
- **Scanning and checking work without a key.** Only submitting reports requires authentication.
207
+ **`discover`, `scan`, and `check` work without a key.** Only `audit`/`submit_report` need one.
166
208
 
167
209
  ---
168
210
 
169
- ## CLI Reference
211
+ ## What it detects
170
212
 
171
- ```
172
- agentaudit discover Find local MCP servers + check registry
173
- agentaudit scan <url> [url...] Quick static scan (regex, ~2s)
174
- agentaudit audit <url> [url...] Deep LLM-powered audit (~30s)
175
- agentaudit check <name> Look up package in registry
176
- agentaudit setup Register + configure API key
177
- ```
213
+ ### Static scan (`scan`)
178
214
 
179
- ### `scan` vs `audit`
180
-
181
- | | `scan` | `audit` |
182
- |--|--------|---------|
183
- | **How** | Regex-based static analysis | LLM 3-pass analysis (UNDERSTAND → DETECT → CLASSIFY) |
184
- | **Speed** | ~2 seconds | ~30 seconds |
185
- | **Depth** | Pattern matching | Semantic code understanding |
186
- | **Needs API key** | No | Yes (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`) |
187
- | **Upload to registry** | No | Yes (with `agentaudit setup`) |
188
-
189
- Use `scan` for quick checks, `audit` for thorough analysis.
190
-
191
- ### Examples
192
-
193
- ```bash
194
- # Discover all MCP servers on your machine
195
- agentaudit discover
215
+ - 🔴 Prompt injection & tool poisoning
216
+ - 🔴 Shell command injection
217
+ - 🔴 SQL injection
218
+ - 🔴 Unsafe deserialization (pickle, YAML)
219
+ - 🟡 Hardcoded secrets
220
+ - 🟡 SSL/TLS verification disabled
221
+ - 🟡 Path traversal
222
+ - 🔵 Wildcard CORS
223
+ - 🔵 Undisclosed telemetry
196
224
 
197
- # Quick scan
198
- agentaudit scan https://github.com/jlowin/fastmcp
225
+ ### Deep audit (`audit` / MCP `audit_package`)
199
226
 
200
- # Deep audit (requires ANTHROPIC_API_KEY or OPENAI_API_KEY)
201
- agentaudit audit https://github.com/jlowin/fastmcp
227
+ Everything above, plus:
202
228
 
203
- # Export audit for manual LLM review (no API key needed)
204
- agentaudit audit https://github.com/owner/repo --export
229
+ - 🔴 Multi-file attack chains (credential harvest exfiltration)
230
+ - 🔴 Agent manipulation (impersonation, capability escalation, jailbreaks)
231
+ - 🔴 MCP-specific: tool description injection, resource traversal, unpinned npx
232
+ - 🟡 Persistence mechanisms (crontab, shell RC, git hooks, systemd)
233
+ - 🟡 Obfuscation (base64 exec, zero-width chars, ANSI escapes)
234
+ - 🟡 Context pollution & indirect prompt injection
205
235
 
206
- # Check registry
207
- agentaudit check mongodb-mcp-server
208
- ```
236
+ 50+ detection patterns across 8 categories. [Full pattern list →](https://github.com/starbuck100/agentaudit-skill)
209
237
 
210
238
  ---
211
239
 
212
240
  ## Requirements
213
241
 
214
242
  - **Node.js 18+**
215
- - **Git** (for cloning repos during scan)
243
+ - **Git** (for cloning repos)
216
244
 
217
245
  ---
218
246
 
219
247
  ## Related
220
248
 
221
- - [agentaudit.dev](https://agentaudit.dev) — Trust registry & audit reports
222
- - [agentaudit-skill](https://github.com/starbuck100/agentaudit-skill) — Full agent skill with gate scripts, detection patterns & peer review
249
+ - **[agentaudit.dev](https://agentaudit.dev)** — Trust registry with 400+ audit reports
250
+ - **[agentaudit-skill](https://github.com/starbuck100/agentaudit-skill)** — Full agent skill with gate scripts, detection patterns & peer review system
223
251
 
224
252
  ---
225
253
 
package/cli.mjs CHANGED
@@ -157,17 +157,26 @@ async function setupCommand() {
157
157
 
158
158
  console.log();
159
159
  console.log(` ${c.bold}Ready!${c.reset} You can now:`);
160
- console.log(` ${c.dim}•${c.reset} Scan packages: ${c.cyan}agentaudit scan <repo-url>${c.reset}`);
161
- console.log(` ${c.dim}•${c.reset} Check registry: ${c.cyan}agentaudit check <name>${c.reset}`);
160
+ console.log(` ${c.dim}•${c.reset} Discover servers: ${c.cyan}agentaudit discover${c.reset}`);
161
+ console.log(` ${c.dim}•${c.reset} Audit packages: ${c.cyan}agentaudit audit <repo-url>${c.reset} ${c.dim}(deep LLM analysis)${c.reset}`);
162
+ console.log(` ${c.dim}•${c.reset} Quick scan: ${c.cyan}agentaudit scan <repo-url>${c.reset} ${c.dim}(regex-based)${c.reset}`);
163
+ console.log(` ${c.dim}•${c.reset} Check registry: ${c.cyan}agentaudit check <name>${c.reset}`);
162
164
  console.log(` ${c.dim}•${c.reset} Submit reports via MCP in Claude/Cursor/Windsurf`);
163
165
  console.log();
164
166
  }
165
167
 
166
168
  // ── Helpers ──────────────────────────────────────────────
167
169
 
170
+ function getVersion() {
171
+ try {
172
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
173
+ return pkg.version || '0.0.0';
174
+ } catch { return '0.0.0'; }
175
+ }
176
+
168
177
  function banner() {
169
178
  console.log();
170
- console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v1.0.0${c.reset}`);
179
+ console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
171
180
  console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
172
181
  console.log();
173
182
  }
@@ -705,6 +714,48 @@ function serverSlug(server) {
705
714
  return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
706
715
  }
707
716
 
717
+ async function resolveSourceUrl(server) {
718
+ // Already have it
719
+ if (server.sourceUrl) return server.sourceUrl;
720
+
721
+ // Try npm registry
722
+ if (server.npmPackage) {
723
+ try {
724
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npmPackage)}`, {
725
+ signal: AbortSignal.timeout(5000),
726
+ });
727
+ if (res.ok) {
728
+ const data = await res.json();
729
+ let repoUrl = data.repository?.url;
730
+ if (repoUrl) {
731
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
732
+ if (repoUrl.startsWith('http')) return repoUrl;
733
+ }
734
+ }
735
+ } catch {}
736
+ // Fallback: npm page
737
+ return `https://www.npmjs.com/package/${server.npmPackage}`;
738
+ }
739
+
740
+ // Try PyPI
741
+ if (server.pyPackage) {
742
+ try {
743
+ const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pyPackage)}/json`, {
744
+ signal: AbortSignal.timeout(5000),
745
+ });
746
+ if (res.ok) {
747
+ const data = await res.json();
748
+ const urls = data.info?.project_urls || {};
749
+ const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
750
+ if (source && source.startsWith('http')) return source;
751
+ }
752
+ } catch {}
753
+ return `https://pypi.org/project/${server.pyPackage}/`;
754
+ }
755
+
756
+ return null;
757
+ }
758
+
708
759
  async function discoverCommand() {
709
760
  console.log(` ${c.bold}Discovering local MCP servers...${c.reset}`);
710
761
  console.log();
@@ -728,6 +779,7 @@ async function discoverCommand() {
728
779
  let checkedServers = 0;
729
780
  let auditedServers = 0;
730
781
  let unauditedServers = 0;
782
+ const unauditedWithUrls = [];
731
783
 
732
784
  for (const config of configs) {
733
785
  const servers = extractServersFromConfig(config.content);
@@ -779,11 +831,18 @@ async function discoverCommand() {
779
831
  console.log(`${pipe} ${riskBadge(riskScore)} Risk ${riskScore} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/skills/${slug}${c.reset}`);
780
832
  } else {
781
833
  unauditedServers++;
834
+ // Resolve source URL
835
+ const resolvedUrl = await resolveSourceUrl(server);
782
836
  console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
783
- console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: agentaudit audit <source-url>${c.reset}`);
837
+ if (resolvedUrl) {
838
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: ${c.cyan}agentaudit audit ${resolvedUrl}${c.reset}`);
839
+ unauditedWithUrls.push({ name: server.name, sourceUrl: resolvedUrl });
840
+ } else {
841
+ console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Source URL unknown — check the package's GitHub/npm page${c.reset}`);
842
+ }
784
843
  }
785
844
 
786
- if (server.sourceUrl) {
845
+ if (server.sourceUrl && !server.sourceUrl.includes('npmjs.com')) {
787
846
  console.log(`${pipe} ${c.dim}source: ${server.sourceUrl}${c.reset}`);
788
847
  }
789
848
  }
@@ -800,8 +859,15 @@ async function discoverCommand() {
800
859
  console.log();
801
860
 
802
861
  if (unauditedServers > 0) {
803
- console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
804
- console.log(` ${c.cyan}agentaudit scan <github-url>${c.reset}`);
862
+ if (unauditedWithUrls.length > 0) {
863
+ console.log(` ${c.dim}To audit unaudited servers:${c.reset}`);
864
+ for (const { name, sourceUrl } of unauditedWithUrls) {
865
+ console.log(` ${c.cyan}agentaudit audit ${sourceUrl}${c.reset} ${c.dim}(${name})${c.reset}`);
866
+ }
867
+ } else {
868
+ console.log(` ${c.dim}To audit unaudited servers, run:${c.reset}`);
869
+ console.log(` ${c.cyan}agentaudit audit <source-url>${c.reset}`);
870
+ }
805
871
  console.log();
806
872
  }
807
873
  }
@@ -857,15 +923,28 @@ async function auditRepo(url) {
857
923
  const openaiKey = process.env.OPENAI_API_KEY;
858
924
 
859
925
  if (!anthropicKey && !openaiKey) {
860
- // No LLM API key — output the prepared audit for piping or MCP use
926
+ // No LLM API key — clear explanation
927
+ console.log();
928
+ console.log(` ${c.yellow}No LLM API key found.${c.reset} The ${c.bold}audit${c.reset} command needs an LLM to analyze code.`);
929
+ console.log();
930
+ console.log(` ${c.bold}Option 1: Set an API key${c.reset}`);
931
+ console.log(` Supported keys: ${c.cyan}ANTHROPIC_API_KEY${c.reset} or ${c.cyan}OPENAI_API_KEY${c.reset}`);
932
+ console.log();
933
+ console.log(` ${c.dim}# Linux / macOS:${c.reset}`);
934
+ console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
935
+ console.log(` ${c.dim}export OPENAI_API_KEY=sk-...${c.reset}`);
936
+ console.log();
937
+ console.log(` ${c.dim}# Windows (PowerShell):${c.reset}`);
938
+ console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
939
+ console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
861
940
  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}`);
941
+ console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
942
+ console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
943
+ console.log(` ${c.dim}Creates a markdown file you can paste into any LLM (Claude, ChatGPT, etc.)${c.reset}`);
865
944
  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}`);
945
+ console.log(` ${c.bold}Option 3: Use MCP in Claude/Cursor/Windsurf (no API key needed)${c.reset}`);
946
+ console.log(` ${c.dim}Add AgentAudit as MCP server — your editor's agent runs the audit using its own LLM.${c.reset}`);
947
+ console.log(` ${c.dim}Config: { "mcpServers": { "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } } }${c.reset}`);
869
948
  console.log();
870
949
 
871
950
  // Check if --export flag
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,129 @@ 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 resolveSourceUrl(server) {
194
+ if (server.npm_package) {
195
+ try {
196
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(server.npm_package)}`, {
197
+ signal: AbortSignal.timeout(5000),
198
+ });
199
+ if (res.ok) {
200
+ const data = await res.json();
201
+ let repoUrl = data.repository?.url;
202
+ if (repoUrl) {
203
+ repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
204
+ if (repoUrl.startsWith('http')) return repoUrl;
205
+ }
206
+ }
207
+ } catch {}
208
+ return `https://www.npmjs.com/package/${server.npm_package}`;
209
+ }
210
+ if (server.pip_package) {
211
+ try {
212
+ const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(server.pip_package)}/json`, {
213
+ signal: AbortSignal.timeout(5000),
214
+ });
215
+ if (res.ok) {
216
+ const data = await res.json();
217
+ const urls = data.info?.project_urls || {};
218
+ const source = urls.Source || urls.Repository || urls.Homepage || urls['Source Code'] || data.info?.home_page;
219
+ if (source && source.startsWith('http')) return source;
220
+ }
221
+ } catch {}
222
+ return `https://pypi.org/project/${server.pip_package}/`;
223
+ }
224
+ return null;
225
+ }
226
+
227
+ async function checkRegistry(slug) {
228
+ try {
229
+ const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(slug)}`, {
230
+ signal: AbortSignal.timeout(5000),
231
+ });
232
+ if (res.ok) return await res.json();
233
+ } catch {}
234
+ return null;
235
+ }
236
+
165
237
  // ── MCP Server ───────────────────────────────────────────
166
238
 
167
239
  const server = new Server(
168
- { name: 'agentaudit', version: '1.0.0' },
240
+ { name: 'agentaudit', version: '3.2.0' },
169
241
  { capabilities: { tools: {} } }
170
242
  );
171
243
 
172
244
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
173
245
  tools: [
246
+ {
247
+ name: 'discover_servers',
248
+ 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.',
249
+ inputSchema: {
250
+ type: 'object',
251
+ properties: {
252
+ check_registry: {
253
+ type: 'boolean',
254
+ description: 'If true, also check each discovered server against the AgentAudit registry (default: true)',
255
+ },
256
+ },
257
+ },
258
+ },
174
259
  {
175
260
  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.',
261
+ 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
262
  inputSchema: {
178
263
  type: 'object',
179
264
  properties: {
@@ -187,13 +272,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
187
272
  },
188
273
  {
189
274
  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.',
275
+ 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
276
  inputSchema: {
192
277
  type: 'object',
193
278
  properties: {
194
279
  report: {
195
280
  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.',
281
+ 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
282
  },
198
283
  },
199
284
  required: ['report'],
@@ -201,7 +286,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
201
286
  },
202
287
  {
203
288
  name: 'check_package',
204
- description: 'Look up a package in the AgentAudit security registry. Returns the latest audit results if available.',
289
+ 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
290
  inputSchema: {
206
291
  type: 'object',
207
292
  properties: {
@@ -218,39 +303,106 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
218
303
 
219
304
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
220
305
  const { name, arguments: args } = request.params;
221
-
306
+
222
307
  switch (name) {
308
+
309
+ // ── discover_servers ──────────────────────────────────
310
+ case 'discover_servers': {
311
+ const doRegistryCheck = args.check_registry !== false;
312
+ const configs = discoverMcpServers();
313
+ const foundConfigs = configs.filter(c => c.status === 'found');
314
+ const allServers = foundConfigs.flatMap(c => c.servers.map(s => ({ ...s, platform: c.platform })));
315
+
316
+ let text = `# Discovered MCP Servers\n\n`;
317
+ text += `Scanned ${configs.length} config locations. Found ${foundConfigs.length} config(s) with ${allServers.length} server(s).\n\n`;
318
+
319
+ for (const config of configs) {
320
+ if (config.status === 'not found') continue;
321
+ text += `## ${config.platform}\n`;
322
+ text += `Config: \`${config.config_path}\`\n\n`;
323
+
324
+ if (config.servers.length === 0) {
325
+ text += `No servers configured.\n\n`;
326
+ continue;
327
+ }
328
+
329
+ for (const srv of config.servers) {
330
+ const slug = srv.npm_package?.replace(/^@/, '').replace(/\//g, '-')
331
+ || srv.pip_package?.replace(/[^a-z0-9-]/gi, '-')
332
+ || srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
333
+
334
+ text += `### ${srv.name}\n`;
335
+ text += `- Command: \`${[srv.command, ...srv.args].join(' ')}\`\n`;
336
+ if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
337
+ if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
338
+
339
+ if (doRegistryCheck) {
340
+ const regData = await checkRegistry(slug);
341
+ if (regData) {
342
+ const risk = regData.risk_score ?? regData.latest_risk_score ?? 0;
343
+ const official = regData.has_official_audit ? ' (official)' : '';
344
+ text += `- **Registry: ✅ Audited** — Risk ${risk}/100${official}\n`;
345
+ text += `- Report: ${REGISTRY_URL}/skills/${slug}\n`;
346
+ } else {
347
+ const sourceUrl = await resolveSourceUrl(srv);
348
+ text += `- **Registry: ⚠️ Not audited** — no audit report found\n`;
349
+ if (sourceUrl) {
350
+ text += `- Source: ${sourceUrl}\n`;
351
+ text += `- To audit: call \`audit_package\` with source_url \`${sourceUrl}\`\n`;
352
+ } else {
353
+ text += `- Source URL unknown — check the package's GitHub/npm page\n`;
354
+ text += `- To audit: find the source URL, then call \`audit_package\`\n`;
355
+ }
356
+ }
357
+ }
358
+ text += `\n`;
359
+ }
360
+ }
361
+
362
+ if (allServers.length === 0) {
363
+ text += `No MCP servers found. Config locations searched:\n`;
364
+ text += `- Claude Desktop: ~/.claude/mcp.json\n`;
365
+ text += `- Cursor: ~/.cursor/mcp.json\n`;
366
+ text += `- Windsurf: ~/.codeium/windsurf/mcp_config.json\n`;
367
+ text += `- VS Code: ~/.vscode/mcp.json\n`;
368
+ }
369
+
370
+ return { content: [{ type: 'text', text }] };
371
+ }
372
+
373
+ // ── audit_package ─────────────────────────────────────
223
374
  case 'audit_package': {
224
375
  const { source_url } = args;
225
376
  if (!source_url || !source_url.startsWith('http')) {
226
377
  return { content: [{ type: 'text', text: 'Error: source_url must be a valid HTTP(S) URL' }] };
227
378
  }
228
-
379
+
229
380
  let repoPath;
230
381
  try {
231
382
  repoPath = cloneRepo(source_url);
232
383
  const files = collectFiles(repoPath);
233
384
  const slug = slugFromUrl(source_url);
234
385
  const auditPrompt = loadAuditPrompt();
235
-
236
- // Build the response
386
+
237
387
  let codeBlock = '';
238
388
  for (const file of files) {
239
389
  codeBlock += `\n### FILE: ${file.path}\n\`\`\`\n${file.content}\n\`\`\`\n`;
240
390
  }
241
-
391
+
242
392
  const response = [
243
- `# Security Audit Request`,
393
+ `# Security Audit: ${slug}`,
244
394
  ``,
245
- `**Package:** ${slug}`,
246
395
  `**Source:** ${source_url}`,
247
396
  `**Files collected:** ${files.length}`,
248
397
  ``,
249
- `## Instructions`,
398
+ `## Your Task`,
250
399
  ``,
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.`,
400
+ `1. Analyze the source code below using the 3-pass audit methodology`,
401
+ `2. Call \`submit_report\` with your findings as JSON`,
252
402
  ``,
253
- `The report JSON must include:`,
403
+ `## Report Format`,
404
+ ``,
405
+ `Your report JSON must include:`,
254
406
  '```json',
255
407
  `{`,
256
408
  ` "skill_slug": "${slug}",`,
@@ -285,7 +437,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
285
437
  ``,
286
438
  codeBlock,
287
439
  ].join('\n');
288
-
440
+
289
441
  return { content: [{ type: 'text', text: response }] };
290
442
  } catch (err) {
291
443
  return { content: [{ type: 'text', text: `Error: ${err.message}` }] };
@@ -293,27 +445,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
293
445
  if (repoPath) cleanupRepo(repoPath);
294
446
  }
295
447
  }
296
-
448
+
449
+ // ── submit_report ─────────────────────────────────────
297
450
  case 'submit_report': {
298
451
  const { report } = args;
299
452
  if (!report || typeof report !== 'object') {
300
453
  return { content: [{ type: 'text', text: 'Error: report must be a JSON object' }] };
301
454
  }
302
-
455
+
303
456
  const apiKey = loadApiKey();
304
457
  if (!apiKey) {
305
- return { content: [{ type: 'text', text: 'Error: No API key configured. Set AGENTAUDIT_API_KEY or register first.' }] };
458
+ return { content: [{ type: 'text', text: 'Error: No API key configured. Run `npx agentaudit setup` or set AGENTAUDIT_API_KEY.' }] };
306
459
  }
307
-
308
- // Validate required fields
460
+
309
461
  const required = ['skill_slug', 'source_url', 'risk_score', 'result'];
310
462
  for (const field of required) {
311
463
  if (report[field] == null) {
312
464
  return { content: [{ type: 'text', text: `Error: Missing required field "${field}" in report` }] };
313
465
  }
314
466
  }
315
-
316
- // Auto-fix findings
467
+
317
468
  if (!Array.isArray(report.findings)) report.findings = [];
318
469
  report.findings_count = report.findings.length;
319
470
  if (!report.max_severity) {
@@ -324,7 +475,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
324
475
  return fi < mi ? f.severity : max;
325
476
  }, 'none');
326
477
  }
327
-
478
+
328
479
  try {
329
480
  const res = await fetch(`${REGISTRY_URL}/api/reports`, {
330
481
  method: 'POST',
@@ -335,13 +486,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
335
486
  body: JSON.stringify(report),
336
487
  signal: AbortSignal.timeout(60_000),
337
488
  });
338
-
489
+
339
490
  const body = await res.text();
340
491
  let data;
341
492
  try { data = JSON.parse(body); } catch { data = { raw: body }; }
342
-
493
+
343
494
  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)}` }] };
495
+ 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
496
  } else {
346
497
  return { content: [{ type: 'text', text: `Upload failed (HTTP ${res.status}): ${JSON.stringify(data, null, 2)}` }] };
347
498
  }
@@ -349,31 +500,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
349
500
  return { content: [{ type: 'text', text: `Upload error: ${err.message}` }] };
350
501
  }
351
502
  }
352
-
503
+
504
+ // ── check_package ─────────────────────────────────────
353
505
  case 'check_package': {
354
506
  const { package_name } = args;
355
507
  if (!package_name) {
356
508
  return { content: [{ type: 'text', text: 'Error: package_name is required' }] };
357
509
  }
358
-
510
+
359
511
  try {
360
512
  const res = await fetch(`${REGISTRY_URL}/api/skills/${encodeURIComponent(package_name)}`, {
361
513
  signal: AbortSignal.timeout(10_000),
362
514
  });
363
-
515
+
364
516
  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.` }] };
517
+ 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
518
  }
367
-
519
+
368
520
  const data = await res.json();
369
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
521
+ const risk = data.risk_score ?? data.latest_risk_score ?? 'unknown';
522
+ const official = data.has_official_audit ? '✅ Officially audited' : 'Community audit';
523
+
524
+ let summary = `# ${package_name}\n\n`;
525
+ summary += `**Risk Score:** ${risk}/100\n`;
526
+ summary += `**Status:** ${official}\n`;
527
+ if (data.source_url) summary += `**Source:** ${data.source_url}\n`;
528
+ summary += `**Registry:** ${REGISTRY_URL}/skills/${package_name}\n\n`;
529
+ summary += `## Full Data\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``;
530
+
531
+ return { content: [{ type: 'text', text: summary }] };
370
532
  } catch (err) {
371
533
  return { content: [{ type: 'text', text: `Registry lookup failed: ${err.message}` }] };
372
534
  }
373
535
  }
374
-
536
+
375
537
  default:
376
- return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
538
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}. Available: discover_servers, audit_package, submit_report, check_package` }] };
377
539
  }
378
540
  });
379
541
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentaudit",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Security scanner for AI packages — MCP server + CLI",
5
5
  "type": "module",
6
6
  "bin": {