agentaudit 3.4.0 → 3.6.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/cli.mjs +85 -9
- package/index.mjs +41 -4
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import fs from 'fs';
|
|
17
|
+
import os from 'os';
|
|
17
18
|
import path from 'path';
|
|
18
19
|
import { execSync } from 'child_process';
|
|
19
20
|
import { createInterface } from 'readline';
|
|
@@ -573,15 +574,18 @@ async function scanRepo(url) {
|
|
|
573
574
|
process.stdout.write(`${icons.scan} Scanning ${c.bold}${slug}${c.reset} ${c.dim}...${c.reset}`);
|
|
574
575
|
|
|
575
576
|
// Clone
|
|
576
|
-
const tmpDir = fs.mkdtempSync('
|
|
577
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
577
578
|
const repoPath = path.join(tmpDir, 'repo');
|
|
578
579
|
try {
|
|
579
|
-
execSync(`git clone --depth 1 "${url}" "${repoPath}"
|
|
580
|
+
execSync(`git clone --depth 1 "${url}" "${repoPath}"`, {
|
|
580
581
|
timeout: 30_000,
|
|
581
582
|
stdio: 'pipe',
|
|
582
583
|
});
|
|
583
584
|
} catch (err) {
|
|
584
585
|
process.stdout.write(` ${c.red}✖ clone failed${c.reset}\n`);
|
|
586
|
+
const msg = err.stderr?.toString().trim() || err.message?.split('\n')[0] || '';
|
|
587
|
+
if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
|
|
588
|
+
console.log(` ${c.dim}Make sure git is installed and the URL is accessible.${c.reset}`);
|
|
585
589
|
return null;
|
|
586
590
|
}
|
|
587
591
|
|
|
@@ -598,7 +602,7 @@ async function scanRepo(url) {
|
|
|
598
602
|
const registryData = await checkRegistry(slug);
|
|
599
603
|
|
|
600
604
|
// Cleanup
|
|
601
|
-
try {
|
|
605
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
602
606
|
|
|
603
607
|
const duration = elapsed(start);
|
|
604
608
|
|
|
@@ -702,6 +706,19 @@ function extractServersFromConfig(config) {
|
|
|
702
706
|
const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
|
|
703
707
|
if (pyMatch) info.pyPackage = pyMatch[1];
|
|
704
708
|
|
|
709
|
+
// URL-based MCP server (remote HTTP)
|
|
710
|
+
if (info.url && !info.npmPackage && !info.pyPackage) {
|
|
711
|
+
try {
|
|
712
|
+
const parsed = new URL(info.url);
|
|
713
|
+
// Extract service name from hostname: mcp.supabase.com → supabase
|
|
714
|
+
const hostParts = parsed.hostname.split('.');
|
|
715
|
+
if (hostParts.length >= 2) {
|
|
716
|
+
const serviceName = hostParts.length === 3 ? hostParts[1] : hostParts[0];
|
|
717
|
+
info.remoteService = serviceName;
|
|
718
|
+
}
|
|
719
|
+
} catch {}
|
|
720
|
+
}
|
|
721
|
+
|
|
705
722
|
result.push(info);
|
|
706
723
|
}
|
|
707
724
|
return result;
|
|
@@ -753,6 +770,39 @@ async function resolveSourceUrl(server) {
|
|
|
753
770
|
return `https://pypi.org/project/${server.pyPackage}/`;
|
|
754
771
|
}
|
|
755
772
|
|
|
773
|
+
// URL-based remote MCP server — try GitHub search by service name
|
|
774
|
+
if (server.remoteService) {
|
|
775
|
+
// Try npm registry with common MCP naming patterns
|
|
776
|
+
for (const tryName of [
|
|
777
|
+
`@${server.remoteService}/mcp-server-${server.remoteService}`,
|
|
778
|
+
`${server.remoteService}-mcp`,
|
|
779
|
+
`mcp-server-${server.remoteService}`,
|
|
780
|
+
server.remoteService,
|
|
781
|
+
]) {
|
|
782
|
+
try {
|
|
783
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
|
|
784
|
+
signal: AbortSignal.timeout(3000),
|
|
785
|
+
});
|
|
786
|
+
if (res.ok) {
|
|
787
|
+
const data = await res.json();
|
|
788
|
+
let repoUrl = data.repository?.url;
|
|
789
|
+
if (repoUrl) {
|
|
790
|
+
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^ssh:\/\/git@github\.com/, 'https://github.com');
|
|
791
|
+
if (repoUrl.startsWith('http')) return repoUrl;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch {}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Last resort: if server has a url, show it as context
|
|
799
|
+
if (server.url) {
|
|
800
|
+
try {
|
|
801
|
+
const parsed = new URL(server.url);
|
|
802
|
+
return `https://github.com/search?q=${encodeURIComponent(parsed.hostname + ' MCP')}&type=repositories`;
|
|
803
|
+
} catch {}
|
|
804
|
+
}
|
|
805
|
+
|
|
756
806
|
return null;
|
|
757
807
|
}
|
|
758
808
|
|
|
@@ -821,6 +871,7 @@ async function discoverCommand() {
|
|
|
821
871
|
let sourceLabel = '';
|
|
822
872
|
if (server.npmPackage) sourceLabel = `${c.dim}npm:${server.npmPackage}${c.reset}`;
|
|
823
873
|
else if (server.pyPackage) sourceLabel = `${c.dim}pip:${server.pyPackage}${c.reset}`;
|
|
874
|
+
else if (server.url) sourceLabel = `${c.dim}${server.url.length > 60 ? server.url.slice(0, 57) + '...' : server.url}${c.reset}`;
|
|
824
875
|
else if (server.command) sourceLabel = `${c.dim}${[server.command, ...server.args.slice(0, 2)].join(' ')}${c.reset}`;
|
|
825
876
|
|
|
826
877
|
if (regData) {
|
|
@@ -890,15 +941,18 @@ async function auditRepo(url) {
|
|
|
890
941
|
|
|
891
942
|
// Step 1: Clone
|
|
892
943
|
process.stdout.write(` ${c.dim}[1/4]${c.reset} Cloning repository...`);
|
|
893
|
-
const tmpDir = fs.mkdtempSync('
|
|
944
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
894
945
|
const repoPath = path.join(tmpDir, 'repo');
|
|
895
946
|
try {
|
|
896
|
-
execSync(`git clone --depth 1 "${url}" "${repoPath}"
|
|
947
|
+
execSync(`git clone --depth 1 "${url}" "${repoPath}"`, {
|
|
897
948
|
timeout: 30_000, stdio: 'pipe',
|
|
898
949
|
});
|
|
899
950
|
console.log(` ${c.green}done${c.reset}`);
|
|
900
|
-
} catch {
|
|
951
|
+
} catch (err) {
|
|
901
952
|
console.log(` ${c.red}failed${c.reset}`);
|
|
953
|
+
const msg = err.stderr?.toString().trim() || err.message?.split('\n')[0] || '';
|
|
954
|
+
if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
|
|
955
|
+
console.log(` ${c.dim}Make sure git is installed and the URL is accessible.${c.reset}`);
|
|
902
956
|
return null;
|
|
903
957
|
}
|
|
904
958
|
|
|
@@ -938,6 +992,10 @@ async function auditRepo(url) {
|
|
|
938
992
|
console.log(` ${c.dim}$env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
|
|
939
993
|
console.log(` ${c.dim}$env:OPENAI_API_KEY = "sk-..."${c.reset}`);
|
|
940
994
|
console.log();
|
|
995
|
+
console.log(` ${c.dim}# Windows (CMD):${c.reset}`);
|
|
996
|
+
console.log(` ${c.dim}set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
997
|
+
console.log(` ${c.dim}set OPENAI_API_KEY=sk-...${c.reset}`);
|
|
998
|
+
console.log();
|
|
941
999
|
console.log(` ${c.bold}Option 2: Export for manual review${c.reset}`);
|
|
942
1000
|
console.log(` ${c.cyan}agentaudit audit ${url} --export${c.reset}`);
|
|
943
1001
|
console.log(` ${c.dim}Creates a markdown file you can paste into any LLM (Claude, ChatGPT, etc.)${c.reset}`);
|
|
@@ -976,7 +1034,7 @@ async function auditRepo(url) {
|
|
|
976
1034
|
}
|
|
977
1035
|
|
|
978
1036
|
// Cleanup
|
|
979
|
-
try {
|
|
1037
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
980
1038
|
return null;
|
|
981
1039
|
}
|
|
982
1040
|
|
|
@@ -1050,12 +1108,12 @@ async function auditRepo(url) {
|
|
|
1050
1108
|
} catch (err) {
|
|
1051
1109
|
console.log(` ${c.red}failed${c.reset}`);
|
|
1052
1110
|
console.log(` ${c.red}${err.message}${c.reset}`);
|
|
1053
|
-
try {
|
|
1111
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1054
1112
|
return null;
|
|
1055
1113
|
}
|
|
1056
1114
|
|
|
1057
1115
|
// Cleanup repo
|
|
1058
|
-
try {
|
|
1116
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
1059
1117
|
|
|
1060
1118
|
if (!report) {
|
|
1061
1119
|
console.log(` ${c.red}Could not parse LLM response as JSON${c.reset}`);
|
|
@@ -1142,6 +1200,11 @@ async function checkPackage(name) {
|
|
|
1142
1200
|
async function main() {
|
|
1143
1201
|
const args = process.argv.slice(2);
|
|
1144
1202
|
|
|
1203
|
+
if (args[0] === '-v' || args[0] === '--version') {
|
|
1204
|
+
console.log(`agentaudit ${getVersion()}`);
|
|
1205
|
+
process.exit(0);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1145
1208
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
1146
1209
|
banner();
|
|
1147
1210
|
console.log(` ${c.bold}Commands:${c.reset}`);
|
|
@@ -1162,6 +1225,19 @@ async function main() {
|
|
|
1162
1225
|
console.log(` agentaudit audit https://github.com/owner/repo`);
|
|
1163
1226
|
console.log(` agentaudit check fastmcp`);
|
|
1164
1227
|
console.log();
|
|
1228
|
+
console.log(` ${c.bold}For deep audits,${c.reset} set an LLM API key:`);
|
|
1229
|
+
if (process.platform === 'win32') {
|
|
1230
|
+
console.log(` ${c.dim}PowerShell: $env:ANTHROPIC_API_KEY = "sk-ant-..."${c.reset}`);
|
|
1231
|
+
console.log(` ${c.dim}CMD: set ANTHROPIC_API_KEY=sk-ant-...${c.reset}`);
|
|
1232
|
+
console.log(` ${c.dim}(or use OPENAI_API_KEY instead)${c.reset}`);
|
|
1233
|
+
} else {
|
|
1234
|
+
console.log(` ${c.dim}export ANTHROPIC_API_KEY=sk-ant-...${c.reset} ${c.dim}(or OPENAI_API_KEY)${c.reset}`);
|
|
1235
|
+
}
|
|
1236
|
+
console.log();
|
|
1237
|
+
console.log(` ${c.bold}Or use as MCP server${c.reset} in Cursor/Claude ${c.dim}(no extra API key needed):${c.reset}`);
|
|
1238
|
+
console.log(` ${c.dim}Add to your MCP config:${c.reset}`);
|
|
1239
|
+
console.log(` ${c.dim}{ "agentaudit": { "command": "npx", "args": ["-y", "agentaudit"] } }${c.reset}`);
|
|
1240
|
+
console.log();
|
|
1165
1241
|
process.exit(0);
|
|
1166
1242
|
}
|
|
1167
1243
|
|
package/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
ListToolsRequestSchema,
|
|
26
26
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
27
27
|
import fs from 'fs';
|
|
28
|
+
import os from 'os';
|
|
28
29
|
import path from 'path';
|
|
29
30
|
import { execSync } from 'child_process';
|
|
30
31
|
import { fileURLToPath } from 'url';
|
|
@@ -124,9 +125,9 @@ function collectFiles(dir, basePath = '', collected = [], totalSize = { bytes: 0
|
|
|
124
125
|
// ── Repo Helpers ────────────────────────────────────────
|
|
125
126
|
|
|
126
127
|
function cloneRepo(sourceUrl) {
|
|
127
|
-
const tmpDir = fs.mkdtempSync('
|
|
128
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
128
129
|
try {
|
|
129
|
-
execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo"
|
|
130
|
+
execSync(`git clone --depth 1 "${sourceUrl}" "${tmpDir}/repo"`, {
|
|
130
131
|
timeout: 30_000, stdio: 'pipe',
|
|
131
132
|
});
|
|
132
133
|
return path.join(tmpDir, 'repo');
|
|
@@ -136,7 +137,7 @@ function cloneRepo(sourceUrl) {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
function cleanupRepo(repoPath) {
|
|
139
|
-
try {
|
|
140
|
+
try { fs.rmSync(path.dirname(repoPath), { recursive: true, force: true }); } catch {}
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
function slugFromUrl(url) {
|
|
@@ -176,12 +177,21 @@ function discoverMcpServers() {
|
|
|
176
177
|
const allArgs = [cfg.command, ...(cfg.args || [])].filter(Boolean).join(' ');
|
|
177
178
|
const npxMatch = allArgs.match(/npx\s+(?:-y\s+)?(@?[a-z0-9][\w./-]*)/i);
|
|
178
179
|
const pyMatch = allArgs.match(/(?:uvx|pip run|python -m)\s+(@?[a-z0-9][\w./-]*)/i);
|
|
180
|
+
let remoteService = null;
|
|
181
|
+
if (cfg.url) {
|
|
182
|
+
try {
|
|
183
|
+
const hostParts = new URL(cfg.url).hostname.split('.');
|
|
184
|
+
remoteService = hostParts.length === 3 ? hostParts[1] : hostParts[0];
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
179
187
|
servers.push({
|
|
180
188
|
name,
|
|
181
189
|
command: cfg.command || null,
|
|
182
190
|
args: cfg.args || [],
|
|
191
|
+
url: cfg.url || null,
|
|
183
192
|
npm_package: npxMatch?.[1] || null,
|
|
184
193
|
pip_package: pyMatch?.[1] || null,
|
|
194
|
+
remote_service: remoteService,
|
|
185
195
|
});
|
|
186
196
|
}
|
|
187
197
|
results.push({ platform: c.platform, config_path: c.path, status: 'found', server_count: servers.length, servers });
|
|
@@ -221,6 +231,28 @@ async function resolveSourceUrl(server) {
|
|
|
221
231
|
} catch {}
|
|
222
232
|
return `https://pypi.org/project/${server.pip_package}/`;
|
|
223
233
|
}
|
|
234
|
+
// URL-based remote MCP — try npm with common naming patterns
|
|
235
|
+
if (server.remote_service) {
|
|
236
|
+
for (const tryName of [
|
|
237
|
+
`@${server.remote_service}/mcp-server-${server.remote_service}`,
|
|
238
|
+
`${server.remote_service}-mcp`,
|
|
239
|
+
`mcp-server-${server.remote_service}`,
|
|
240
|
+
]) {
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(tryName)}`, {
|
|
243
|
+
signal: AbortSignal.timeout(3000),
|
|
244
|
+
});
|
|
245
|
+
if (res.ok) {
|
|
246
|
+
const data = await res.json();
|
|
247
|
+
let repoUrl = data.repository?.url;
|
|
248
|
+
if (repoUrl) {
|
|
249
|
+
repoUrl = repoUrl.replace(/^git\+/, '').replace(/\.git$/, '');
|
|
250
|
+
if (repoUrl.startsWith('http')) return repoUrl;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
224
256
|
return null;
|
|
225
257
|
}
|
|
226
258
|
|
|
@@ -332,9 +364,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
332
364
|
|| srv.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
333
365
|
|
|
334
366
|
text += `### ${srv.name}\n`;
|
|
335
|
-
|
|
367
|
+
if (srv.url) {
|
|
368
|
+
text += `- URL: \`${srv.url}\`\n`;
|
|
369
|
+
} else {
|
|
370
|
+
text += `- Command: \`${[srv.command, ...srv.args].filter(Boolean).join(' ')}\`\n`;
|
|
371
|
+
}
|
|
336
372
|
if (srv.npm_package) text += `- npm: ${srv.npm_package}\n`;
|
|
337
373
|
if (srv.pip_package) text += `- pip: ${srv.pip_package}\n`;
|
|
374
|
+
if (srv.remote_service) text += `- Service: ${srv.remote_service}\n`;
|
|
338
375
|
|
|
339
376
|
if (doRegistryCheck) {
|
|
340
377
|
const regData = await checkRegistry(slug);
|