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.
- package/README.md +152 -124
- package/cli.mjs +93 -14
- package/index.mjs +234 -72
- 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
|
|
7
|
+
Scan MCP servers, agent skills, and AI tools for vulnerabilities.
|
|
8
|
+
MCP server for agents + standalone CLI for humans.
|
|
8
9
|
|
|
9
10
|
[](https://www.npmjs.com/package/agentaudit)
|
|
10
|
-
[](https://agentaudit.dev)
|
|
11
12
|
[](LICENSE)
|
|
12
13
|
|
|
13
14
|
</div>
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## Getting Started
|
|
18
19
|
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
agentaudit scan https://github.com/owner/repo
|
|
22
|
+
### Option A: MCP Server in your AI editor (recommended)
|
|
28
23
|
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"agentaudit": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "agentaudit"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
│
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
+
Add AgentAudit to your AI editor. Your agent gets 4 tools:
|
|
80
132
|
|
|
81
|
-
| Tool | Description |
|
|
82
|
-
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
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
|
-
###
|
|
140
|
+
### Setup
|
|
88
141
|
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
### How the MCP audit works
|
|
180
|
+
### How an audit works via MCP
|
|
132
181
|
|
|
133
182
|
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Agent
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
+
**`discover`, `scan`, and `check` work without a key.** Only `audit`/`submit_report` need one.
|
|
166
208
|
|
|
167
209
|
---
|
|
168
210
|
|
|
169
|
-
##
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
198
|
-
agentaudit scan https://github.com/jlowin/fastmcp
|
|
225
|
+
### Deep audit (`audit` / MCP `audit_package`)
|
|
199
226
|
|
|
200
|
-
|
|
201
|
-
agentaudit audit https://github.com/jlowin/fastmcp
|
|
227
|
+
Everything above, plus:
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
|
243
|
+
- **Git** (for cloning repos)
|
|
216
244
|
|
|
217
245
|
---
|
|
218
246
|
|
|
219
247
|
## Related
|
|
220
248
|
|
|
221
|
-
- [agentaudit.dev](https://agentaudit.dev) — Trust registry
|
|
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}
|
|
161
|
-
console.log(` ${c.dim}•${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}
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
|
|
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 —
|
|
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.
|
|
863
|
-
console.log(`
|
|
864
|
-
console.log(`
|
|
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}
|
|
867
|
-
console.log(`
|
|
868
|
-
console.log(`
|
|
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
|
-
*
|
|
5
|
+
* Security audit capabilities via Model Context Protocol.
|
|
6
6
|
*
|
|
7
7
|
* Tools:
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
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
|
-
*
|
|
14
|
+
* npx agentaudit (starts MCP server via stdio)
|
|
15
|
+
* node index.mjs (same)
|
|
14
16
|
*
|
|
15
|
-
* Configure in Claude/Cursor/
|
|
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;
|
|
41
|
-
const MAX_TOTAL_SIZE = 300_000;
|
|
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
|
-
// ──
|
|
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: '
|
|
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: '
|
|
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.
|
|
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: '
|
|
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
|
|
393
|
+
`# Security Audit: ${slug}`,
|
|
244
394
|
``,
|
|
245
|
-
`**Package:** ${slug}`,
|
|
246
395
|
`**Source:** ${source_url}`,
|
|
247
396
|
`**Files collected:** ${files.length}`,
|
|
248
397
|
``,
|
|
249
|
-
`##
|
|
398
|
+
`## Your Task`,
|
|
250
399
|
``,
|
|
251
|
-
`Analyze the source code below
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|