ledd-mcp-audit-server 2.0.0 → 2.0.2
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/CHANGELOG.md +19 -0
- package/MIGRATION.md +6 -1
- package/README.md +25 -6
- package/cli.js +93 -13
- package/index.js +85 -14
- package/mcp/index.js +173 -9
- package/mcp/server.json +2 -2
- package/package.json +6 -1
- package/server.json +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.2 (2026-03-19)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added official MCP Registry metadata with `mcpName` and root `server.json`.
|
|
7
|
+
- Added registry-ready environment variable metadata for `AGENT_SECURITY_API_KEY` and optional `AGENT_SECURITY_BASE_URL`.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Published package now includes `server.json` for registry/discovery tooling.
|
|
11
|
+
|
|
12
|
+
## 2.0.1 (2026-03-19)
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Managed hosted flow now auto-targets `https://mcpaudit.metaltorque.dev` when `AGENT_SECURITY_API_KEY` is set and no explicit endpoint override is configured.
|
|
16
|
+
- Clearer CLI and MCP auth guidance when the proxy receives a `401 Unauthorized` response.
|
|
17
|
+
- MCP client and CLI docs now show the API-key based hosted setup directly.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Updated the recommended MCP configuration to pass `AGENT_SECURITY_API_KEY` via the client `env` block.
|
|
21
|
+
|
|
3
22
|
## 2.0.0 (2026-03-15)
|
|
4
23
|
|
|
5
24
|
### Breaking Changes
|
package/MIGRATION.md
CHANGED
|
@@ -22,12 +22,17 @@ If you launch the MCP proxy through `npx`, update your client config:
|
|
|
22
22
|
"mcpServers": {
|
|
23
23
|
"mcp-audit-server": {
|
|
24
24
|
"command": "npx",
|
|
25
|
-
"args": ["-y", "ledd-mcp-audit-server", "--mcp"]
|
|
25
|
+
"args": ["-y", "ledd-mcp-audit-server", "--mcp"],
|
|
26
|
+
"env": {
|
|
27
|
+
"AGENT_SECURITY_API_KEY": "your-issued-api-key"
|
|
28
|
+
}
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
```
|
|
30
33
|
|
|
34
|
+
If `AGENT_SECURITY_API_KEY` is set and no endpoint override is provided, the proxy will automatically target `https://mcpaudit.metaltorque.dev`. For self-hosted backends, also set `AGENT_SECURITY_BASE_URL`.
|
|
35
|
+
|
|
31
36
|
## Package Split
|
|
32
37
|
|
|
33
38
|
The old package name was overloaded across two different repos. The split is now:
|
package/README.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Thin MCP server and CLI proxy for AI agent and MCP security auditing. It connects to a private audit API to analyze MCP configurations, test prompt injection resistance, trace data flows, scan packages, and generate security policies.
|
|
4
4
|
|
|
5
|
-
This package is a thin proxy. All scan logic lives in a private backend operated by you or your provider.
|
|
5
|
+
This package is a thin proxy. All scan logic lives in a private backend operated by you or your provider.
|
|
6
|
+
|
|
7
|
+
Managed hosted flow:
|
|
8
|
+
- set `AGENT_SECURITY_API_KEY`
|
|
9
|
+
- the package will automatically target `https://mcpaudit.metaltorque.dev`
|
|
10
|
+
|
|
11
|
+
Self-hosted or private-network flow:
|
|
12
|
+
- set `AGENT_SECURITY_BASE_URL` to your HTTPS API origin
|
|
13
|
+
- or set `AGENT_SECURITY_HOST` and `AGENT_SECURITY_PORT` for a loopback/private deployment
|
|
6
14
|
|
|
7
15
|
Hosted backend access is not bundled with this package. If you want managed access or a licensed private deployment, contact [Ledd Consulting](https://leddconsulting.com).
|
|
8
16
|
|
|
@@ -26,12 +34,17 @@ Add to your MCP client configuration (Claude Desktop, Cursor, etc.):
|
|
|
26
34
|
"mcpServers": {
|
|
27
35
|
"mcp-audit-server": {
|
|
28
36
|
"command": "npx",
|
|
29
|
-
"args": ["-y", "ledd-mcp-audit-server", "--mcp"]
|
|
37
|
+
"args": ["-y", "ledd-mcp-audit-server", "--mcp"],
|
|
38
|
+
"env": {
|
|
39
|
+
"AGENT_SECURITY_API_KEY": "your-issued-api-key"
|
|
40
|
+
}
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
```
|
|
34
45
|
|
|
46
|
+
For a self-hosted backend, add `AGENT_SECURITY_BASE_URL` to that same `env` block.
|
|
47
|
+
|
|
35
48
|
The server exposes 9 tools over stdio:
|
|
36
49
|
|
|
37
50
|
| Tool | Description |
|
|
@@ -51,6 +64,9 @@ The server exposes 9 tools over stdio:
|
|
|
51
64
|
The CLI forwards commands to the private audit API.
|
|
52
65
|
|
|
53
66
|
```bash
|
|
67
|
+
# Hosted quick start
|
|
68
|
+
export AGENT_SECURITY_API_KEY=your-issued-api-key
|
|
69
|
+
|
|
54
70
|
# Audit an MCP configuration file
|
|
55
71
|
mcp-audit-server scan-config ./claude_desktop_config.json
|
|
56
72
|
|
|
@@ -85,14 +101,17 @@ mcp-audit-server scan-config ./config.json --json
|
|
|
85
101
|
mcp-audit-server --mcp
|
|
86
102
|
```
|
|
87
103
|
|
|
104
|
+
For a self-hosted backend, also set `AGENT_SECURITY_BASE_URL=https://your-audit-host`.
|
|
105
|
+
|
|
88
106
|
## Environment Variables
|
|
89
107
|
|
|
90
108
|
| Variable | Default | Description |
|
|
91
109
|
|----------|---------|-------------|
|
|
92
110
|
| `AGENT_SECURITY_BASE_URL` | (none) | Full audit API origin, e.g. `https://audit.example.com` |
|
|
93
|
-
| `AGENT_SECURITY_HOST` | `127.0.0.1` |
|
|
94
|
-
| `AGENT_SECURITY_PORT` | `3091` |
|
|
95
|
-
| `AGENT_SECURITY_API_KEY` | (none) | API key for authenticated access |
|
|
111
|
+
| `AGENT_SECURITY_HOST` | `127.0.0.1` | Self-hosted/private-network audit API host |
|
|
112
|
+
| `AGENT_SECURITY_PORT` | `3091` | Self-hosted/private-network audit API port |
|
|
113
|
+
| `AGENT_SECURITY_API_KEY` | (none) | API key for authenticated access. If set with no endpoint overrides, the package uses `https://mcpaudit.metaltorque.dev` |
|
|
114
|
+
| `AGENT_SECURITY_REQUEST_TIMEOUT_MS` | `15000` | Request timeout for CLI and MCP proxy calls |
|
|
96
115
|
| `AGENT_SECURITY_ADMIN_MODE` | (none) | Set to `1` to enable active server probing |
|
|
97
116
|
|
|
98
117
|
## What It Detects
|
|
@@ -114,7 +133,7 @@ mcp-audit-server --mcp
|
|
|
114
133
|
## Requirements
|
|
115
134
|
|
|
116
135
|
- Node.js >= 18
|
|
117
|
-
- Access to a private audit API. Use `AGENT_SECURITY_BASE_URL` for hosted HTTPS deployments, or `AGENT_SECURITY_HOST` and `AGENT_SECURITY_PORT` for local/private-network deployments.
|
|
136
|
+
- Access to a private audit API. The managed hosted default is `https://mcpaudit.metaltorque.dev` when `AGENT_SECURITY_API_KEY` is set. Use `AGENT_SECURITY_BASE_URL` for other hosted HTTPS deployments, or `AGENT_SECURITY_HOST` and `AGENT_SECURITY_PORT` for local/private-network deployments.
|
|
118
137
|
|
|
119
138
|
## License
|
|
120
139
|
|
package/cli.js
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const { BASE_URL } = require("./index");
|
|
5
|
+
const { BASE_URL, DEFAULT_HOSTED_BASE_URL } = require("./index");
|
|
6
6
|
|
|
7
7
|
const API_KEY = process.env.AGENT_SECURITY_API_KEY || "";
|
|
8
|
+
const ADMIN_MODE_ENABLED = process.env.AGENT_SECURITY_ADMIN_MODE === "1";
|
|
9
|
+
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.AGENT_SECURITY_REQUEST_TIMEOUT_MS || "", 10) || 15_000;
|
|
10
|
+
const SCAN_SERVER_REQUIRES_ADMIN_MESSAGE = "scan-server requires AGENT_SECURITY_ADMIN_MODE=1.";
|
|
8
11
|
|
|
9
12
|
function printUsage() {
|
|
10
13
|
process.stderr.write(
|
|
@@ -27,15 +30,29 @@ function printUsage() {
|
|
|
27
30
|
" --json Output raw JSON instead of formatted tables",
|
|
28
31
|
"",
|
|
29
32
|
"Environment Variables:",
|
|
30
|
-
|
|
31
|
-
" AGENT_SECURITY_HOST
|
|
32
|
-
" AGENT_SECURITY_PORT
|
|
33
|
-
|
|
33
|
+
` AGENT_SECURITY_BASE_URL Full audit API origin, e.g. https://audit.example.com`,
|
|
34
|
+
" AGENT_SECURITY_HOST Self-hosted/private-network host (default: 127.0.0.1)",
|
|
35
|
+
" AGENT_SECURITY_PORT Self-hosted/private-network port (default: 3091)",
|
|
36
|
+
` AGENT_SECURITY_API_KEY API key; if set without endpoint overrides, ${DEFAULT_HOSTED_BASE_URL} is used`,
|
|
37
|
+
" AGENT_SECURITY_REQUEST_TIMEOUT_MS Request timeout in milliseconds (default: 15000)",
|
|
34
38
|
" AGENT_SECURITY_ADMIN_MODE Enable active server probing (set to \"1\")"
|
|
35
39
|
].join("\n") + "\n"
|
|
36
40
|
);
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
function buildUnauthorizedMessage(baseMessage) {
|
|
44
|
+
const message = typeof baseMessage === "string" && baseMessage.trim() ? baseMessage.trim() : "Unauthorized.";
|
|
45
|
+
if (API_KEY) {
|
|
46
|
+
return message;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (BASE_URL === DEFAULT_HOSTED_BASE_URL) {
|
|
50
|
+
return `${message} Set AGENT_SECURITY_API_KEY to use the hosted audit API at ${DEFAULT_HOSTED_BASE_URL}.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `${message} Set AGENT_SECURITY_API_KEY for ${BASE_URL} access.`;
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
async function callApi(method, pathname, payload) {
|
|
40
57
|
const headers = {
|
|
41
58
|
"content-type": "application/json"
|
|
@@ -44,11 +61,24 @@ async function callApi(method, pathname, payload) {
|
|
|
44
61
|
headers["x-api-key"] = API_KEY;
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
66
|
+
let response;
|
|
67
|
+
try {
|
|
68
|
+
response = await fetch(`${BASE_URL}${pathname}`, {
|
|
69
|
+
method,
|
|
70
|
+
headers,
|
|
71
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error && error.name === "AbortError") {
|
|
76
|
+
throw new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms.`);
|
|
77
|
+
}
|
|
78
|
+
throw new Error(error && error.message ? error.message : "Request failed.");
|
|
79
|
+
} finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
}
|
|
52
82
|
|
|
53
83
|
let body;
|
|
54
84
|
try {
|
|
@@ -57,12 +87,47 @@ async function callApi(method, pathname, payload) {
|
|
|
57
87
|
throw new Error(`Request failed with status ${response.status} (non-JSON response).`);
|
|
58
88
|
}
|
|
59
89
|
if (!response.ok) {
|
|
90
|
+
if (response.status === 401) {
|
|
91
|
+
throw new Error(buildUnauthorizedMessage(body && body.error));
|
|
92
|
+
}
|
|
60
93
|
throw new Error(body && body.error ? body.error : `Request failed with status ${response.status}`);
|
|
61
94
|
}
|
|
62
95
|
|
|
63
96
|
return body;
|
|
64
97
|
}
|
|
65
98
|
|
|
99
|
+
function parseCliArgs(argv) {
|
|
100
|
+
const input = Array.isArray(argv) ? argv : [];
|
|
101
|
+
let jsonMode = false;
|
|
102
|
+
let command = "";
|
|
103
|
+
const commandArgs = [];
|
|
104
|
+
|
|
105
|
+
for (const rawArg of input) {
|
|
106
|
+
const arg = String(rawArg || "");
|
|
107
|
+
if (!command) {
|
|
108
|
+
if (arg === "--json") {
|
|
109
|
+
jsonMode = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
command = arg;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (arg === "--json") {
|
|
117
|
+
jsonMode = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
commandArgs.push(arg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
command,
|
|
126
|
+
args: commandArgs,
|
|
127
|
+
jsonMode
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
66
131
|
function formatReport(report) {
|
|
67
132
|
console.table([
|
|
68
133
|
{
|
|
@@ -162,8 +227,8 @@ function formatGeneratePolicy(result) {
|
|
|
162
227
|
}
|
|
163
228
|
|
|
164
229
|
async function main() {
|
|
165
|
-
const
|
|
166
|
-
const jsonMode =
|
|
230
|
+
const parsedArgs = parseCliArgs(process.argv.slice(2));
|
|
231
|
+
const { command, args, jsonMode } = parsedArgs;
|
|
167
232
|
|
|
168
233
|
try {
|
|
169
234
|
if (command === "--help" || command === "-h") {
|
|
@@ -195,6 +260,10 @@ async function main() {
|
|
|
195
260
|
}
|
|
196
261
|
|
|
197
262
|
if (command === "scan-server") {
|
|
263
|
+
if (!ADMIN_MODE_ENABLED) {
|
|
264
|
+
throw new Error(SCAN_SERVER_REQUIRES_ADMIN_MESSAGE);
|
|
265
|
+
}
|
|
266
|
+
|
|
198
267
|
const targetCommand = args[0];
|
|
199
268
|
if (!targetCommand) {
|
|
200
269
|
throw new Error("scan-server requires a command.");
|
|
@@ -339,8 +408,19 @@ async function main() {
|
|
|
339
408
|
|
|
340
409
|
if (require.main === module) {
|
|
341
410
|
if (process.argv.includes("--mcp")) {
|
|
342
|
-
require("./mcp/index.js")
|
|
411
|
+
require("./mcp/index.js").main().catch((error) => {
|
|
412
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
});
|
|
343
415
|
} else {
|
|
344
416
|
main();
|
|
345
417
|
}
|
|
346
418
|
}
|
|
419
|
+
|
|
420
|
+
module.exports = {
|
|
421
|
+
main,
|
|
422
|
+
testOnly: {
|
|
423
|
+
buildUnauthorizedMessage,
|
|
424
|
+
parseCliArgs
|
|
425
|
+
}
|
|
426
|
+
};
|
package/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mcp-audit-server — public entry point
|
|
3
3
|
*
|
|
4
|
-
* This package is a thin MCP interface to a private audit API.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* This package is a thin MCP interface to a private audit API. Local/self-hosted
|
|
5
|
+
* deployments can target a loopback API on http://127.0.0.1:3091, while the
|
|
6
|
+
* managed hosted flow auto-targets https://mcpaudit.metaltorque.dev when an
|
|
7
|
+
* API key is present and no explicit endpoint override is set.
|
|
7
8
|
*
|
|
8
9
|
* Start the MCP server: node mcp/index.js
|
|
9
10
|
* Use the CLI: node cli.js scan-config <file>
|
|
@@ -11,17 +12,55 @@
|
|
|
11
12
|
|
|
12
13
|
const net = require("net");
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
15
|
+
const DEFAULT_HOSTED_BASE_URL = "https://mcpaudit.metaltorque.dev";
|
|
16
|
+
const RAW_BASE_URL = process.env.AGENT_SECURITY_BASE_URL;
|
|
17
|
+
const RAW_HOST = process.env.AGENT_SECURITY_HOST;
|
|
18
|
+
const RAW_PORT = process.env.AGENT_SECURITY_PORT;
|
|
19
|
+
const RAW_API_KEY = process.env.AGENT_SECURITY_API_KEY || "";
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
const PORT = Number.parseInt(RAW_PORT || "", 10) || 3091;
|
|
22
|
+
const HOST = RAW_HOST || "127.0.0.1";
|
|
23
|
+
|
|
24
|
+
function normalizeHostToken(host) {
|
|
18
25
|
const value = String(host || "").trim();
|
|
19
26
|
if (!value) {
|
|
20
|
-
return "
|
|
27
|
+
return "";
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
24
|
-
return value;
|
|
31
|
+
return value.slice(1, -1).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isLoopbackHost(host) {
|
|
38
|
+
const normalized = normalizeHostToken(host).toLowerCase();
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (normalized === "localhost") {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (net.isIP(normalized) === 4) {
|
|
48
|
+
return /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (net.isIP(normalized) === 6) {
|
|
52
|
+
return normalized === "::1" ||
|
|
53
|
+
normalized === "0:0:0:0:0:0:0:1" ||
|
|
54
|
+
/^::ffff:127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function formatHostForUrl(host) {
|
|
61
|
+
const value = normalizeHostToken(host);
|
|
62
|
+
if (!value) {
|
|
63
|
+
return "127.0.0.1";
|
|
25
64
|
}
|
|
26
65
|
|
|
27
66
|
return net.isIP(value) === 6 ? `[${value}]` : value;
|
|
@@ -30,21 +69,53 @@ function formatHostForUrl(host) {
|
|
|
30
69
|
function resolveBaseUrl(options = {}) {
|
|
31
70
|
const configuredBaseUrl = typeof options.baseUrl === "string" ? options.baseUrl.trim() : "";
|
|
32
71
|
if (configuredBaseUrl) {
|
|
33
|
-
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = new URL(configuredBaseUrl);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error("AGENT_SECURITY_BASE_URL must be a valid http:// or https:// URL.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const protocol = parsed.protocol.toLowerCase();
|
|
80
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
34
81
|
throw new Error("AGENT_SECURITY_BASE_URL must start with http:// or https://.");
|
|
35
82
|
}
|
|
83
|
+
if (protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
|
|
84
|
+
throw new Error("AGENT_SECURITY_BASE_URL must use https:// for non-loopback hosts.");
|
|
85
|
+
}
|
|
36
86
|
return configuredBaseUrl.replace(/\/+$/, "");
|
|
37
87
|
}
|
|
38
88
|
|
|
39
|
-
|
|
89
|
+
if (options.useHostedDefault) {
|
|
90
|
+
return DEFAULT_HOSTED_BASE_URL;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const host = typeof options.host === "string" && options.host.trim()
|
|
94
|
+
? options.host
|
|
95
|
+
: HOST;
|
|
40
96
|
const port = Number.isInteger(options.port) ? options.port : PORT;
|
|
97
|
+
if (!isLoopbackHost(host)) {
|
|
98
|
+
throw new Error("Use AGENT_SECURITY_BASE_URL with an https:// origin for non-loopback audit hosts.");
|
|
99
|
+
}
|
|
41
100
|
return `http://${formatHostForUrl(host)}:${port}`;
|
|
42
101
|
}
|
|
43
102
|
|
|
44
103
|
const BASE_URL = resolveBaseUrl({
|
|
45
|
-
baseUrl:
|
|
46
|
-
host:
|
|
47
|
-
port: PORT
|
|
104
|
+
baseUrl: RAW_BASE_URL,
|
|
105
|
+
host: RAW_HOST,
|
|
106
|
+
port: PORT,
|
|
107
|
+
useHostedDefault: !String(RAW_BASE_URL || "").trim() &&
|
|
108
|
+
RAW_HOST === undefined &&
|
|
109
|
+
RAW_PORT === undefined &&
|
|
110
|
+
Boolean(RAW_API_KEY)
|
|
48
111
|
});
|
|
49
112
|
|
|
50
|
-
module.exports = {
|
|
113
|
+
module.exports = {
|
|
114
|
+
PORT,
|
|
115
|
+
HOST,
|
|
116
|
+
BASE_URL,
|
|
117
|
+
DEFAULT_HOSTED_BASE_URL,
|
|
118
|
+
formatHostForUrl,
|
|
119
|
+
isLoopbackHost,
|
|
120
|
+
resolveBaseUrl
|
|
121
|
+
};
|
package/mcp/index.js
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const AUDIT_API_KEY = process.env.AGENT_SECURITY_API_KEY || "";
|
|
9
|
-
const { BASE_URL: AUDIT_BASE_URL } = require("../index");
|
|
9
|
+
const { BASE_URL: AUDIT_BASE_URL, DEFAULT_HOSTED_BASE_URL } = require("../index");
|
|
10
10
|
const { version: APP_VERSION } = require("../package.json");
|
|
11
|
+
const REQUEST_TIMEOUT_MS = Number.parseInt(process.env.AGENT_SECURITY_REQUEST_TIMEOUT_MS || "", 10) || 15_000;
|
|
12
|
+
const ACTIVE_SERVER_PROBING_DISABLED_MESSAGE = "Active server probing is disabled unless AGENT_SECURITY_ADMIN_MODE=1.";
|
|
11
13
|
|
|
12
14
|
const MCP_MAX_REQUESTS_PER_MINUTE = 30;
|
|
13
15
|
const MCP_WINDOW_MS = 60_000;
|
|
@@ -133,11 +135,24 @@ async function callAuditApi(method, apiPath, payload) {
|
|
|
133
135
|
headers["x-api-key"] = AUDIT_API_KEY;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
140
|
+
let response;
|
|
141
|
+
try {
|
|
142
|
+
response = await fetch(`${AUDIT_BASE_URL}${apiPath}`, {
|
|
143
|
+
method,
|
|
144
|
+
headers,
|
|
145
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error && error.name === "AbortError") {
|
|
150
|
+
return { error: `Audit API request timed out after ${REQUEST_TIMEOUT_MS}ms.` };
|
|
151
|
+
}
|
|
152
|
+
return { error: error && error.message ? error.message : "Audit API request failed." };
|
|
153
|
+
} finally {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
141
156
|
|
|
142
157
|
const text = await response.text();
|
|
143
158
|
let body;
|
|
@@ -148,6 +163,19 @@ async function callAuditApi(method, apiPath, payload) {
|
|
|
148
163
|
}
|
|
149
164
|
|
|
150
165
|
if (!response.ok) {
|
|
166
|
+
if (response.status === 401) {
|
|
167
|
+
const baseMessage = body && body.error ? body.error : "Unauthorized.";
|
|
168
|
+
if (!AUDIT_API_KEY && AUDIT_BASE_URL === DEFAULT_HOSTED_BASE_URL) {
|
|
169
|
+
return {
|
|
170
|
+
error: `${baseMessage} Set AGENT_SECURITY_API_KEY to use the hosted audit API at ${DEFAULT_HOSTED_BASE_URL}.`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (!AUDIT_API_KEY) {
|
|
174
|
+
return {
|
|
175
|
+
error: `${baseMessage} Set AGENT_SECURITY_API_KEY for ${AUDIT_BASE_URL} access.`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
151
179
|
return { error: body.error || `Audit API returned status ${response.status}` };
|
|
152
180
|
}
|
|
153
181
|
|
|
@@ -164,6 +192,126 @@ function checkMcpRateLimit() {
|
|
|
164
192
|
return mcpRequestCount <= MCP_MAX_REQUESTS_PER_MINUTE;
|
|
165
193
|
}
|
|
166
194
|
|
|
195
|
+
function isAdminModeEnabled() {
|
|
196
|
+
return process.env.AGENT_SECURITY_ADMIN_MODE === "1";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const severityPenalty = {
|
|
200
|
+
critical: 20,
|
|
201
|
+
high: 12,
|
|
202
|
+
medium: 6,
|
|
203
|
+
low: 2,
|
|
204
|
+
info: 0
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
function dedupeFindings(findings) {
|
|
208
|
+
const deduped = [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
|
|
211
|
+
for (const finding of Array.isArray(findings) ? findings : []) {
|
|
212
|
+
if (!finding || typeof finding !== "object") {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const key = JSON.stringify([
|
|
216
|
+
finding.severity || "",
|
|
217
|
+
finding.source || "",
|
|
218
|
+
finding.cwe || "",
|
|
219
|
+
finding.location || "",
|
|
220
|
+
finding.description || ""
|
|
221
|
+
]);
|
|
222
|
+
if (seen.has(key)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
seen.add(key);
|
|
226
|
+
deduped.push(finding);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return deduped;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function summarizeFindings(findings) {
|
|
233
|
+
const summary = {
|
|
234
|
+
total: 0,
|
|
235
|
+
critical: 0,
|
|
236
|
+
high: 0,
|
|
237
|
+
medium: 0,
|
|
238
|
+
low: 0,
|
|
239
|
+
info: 0
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
for (const finding of findings) {
|
|
243
|
+
summary.total += 1;
|
|
244
|
+
if (Object.prototype.hasOwnProperty.call(summary, finding.severity)) {
|
|
245
|
+
summary[finding.severity] += 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return summary;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function calculateScore(findings) {
|
|
253
|
+
let score = 100;
|
|
254
|
+
for (const finding of findings) {
|
|
255
|
+
score -= severityPenalty[finding.severity] || 0;
|
|
256
|
+
}
|
|
257
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function calculateGrade(score) {
|
|
261
|
+
if (score >= 97) return "A+";
|
|
262
|
+
if (score >= 93) return "A";
|
|
263
|
+
if (score >= 90) return "A-";
|
|
264
|
+
if (score >= 87) return "B+";
|
|
265
|
+
if (score >= 83) return "B";
|
|
266
|
+
if (score >= 80) return "B-";
|
|
267
|
+
if (score >= 77) return "C+";
|
|
268
|
+
if (score >= 73) return "C";
|
|
269
|
+
if (score >= 70) return "C-";
|
|
270
|
+
if (score >= 67) return "D+";
|
|
271
|
+
if (score >= 63) return "D";
|
|
272
|
+
if (score >= 60) return "D-";
|
|
273
|
+
return "F";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function buildExecutiveSummary(summary, score, grade, count) {
|
|
277
|
+
if (!summary.total) {
|
|
278
|
+
return `Composite report completed with no findings. Score ${score}/100 (${grade}).`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const severityParts = [];
|
|
282
|
+
for (const severity of ["critical", "high", "medium", "low", "info"]) {
|
|
283
|
+
if (summary[severity]) {
|
|
284
|
+
severityParts.push(`${summary[severity]} ${severity}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
`Composite report generated from ${count} audit${count === 1 ? "" : "s"} with score ${score}/100 (${grade}).`,
|
|
290
|
+
`${summary.total} deduplicated finding${summary.total === 1 ? "" : "s"} identified${severityParts.length ? ` including ${severityParts.join(", ")}` : ""}.`
|
|
291
|
+
].join(" ");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function combineReports(reports, sourceAuditIds) {
|
|
295
|
+
const findings = dedupeFindings(reports.flatMap((report) => Array.isArray(report.findings) ? report.findings : []));
|
|
296
|
+
const score = calculateScore(findings);
|
|
297
|
+
const grade = calculateGrade(score);
|
|
298
|
+
const findingsSummary = summarizeFindings(findings);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
id: sourceAuditIds.join(","),
|
|
302
|
+
type: "report",
|
|
303
|
+
target: sourceAuditIds.join(", "),
|
|
304
|
+
status: "completed",
|
|
305
|
+
score,
|
|
306
|
+
grade,
|
|
307
|
+
findings,
|
|
308
|
+
findingsSummary,
|
|
309
|
+
sourceAuditIds,
|
|
310
|
+
executiveSummary: buildExecutiveSummary(findingsSummary, score, grade, reports.length),
|
|
311
|
+
generatedAt: new Date().toISOString()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
167
315
|
async function runAuditTool(toolName, args) {
|
|
168
316
|
if (!checkMcpRateLimit()) {
|
|
169
317
|
return { error: "Rate limit exceeded. Try again later." };
|
|
@@ -183,8 +331,16 @@ async function runAuditTool(toolName, args) {
|
|
|
183
331
|
if (ids.length === 1) {
|
|
184
332
|
return callAuditApi("GET", `/report/${encodeURIComponent(ids[0])}`);
|
|
185
333
|
}
|
|
186
|
-
const
|
|
187
|
-
|
|
334
|
+
const reports = await Promise.all(ids.map((id) => callAuditApi("GET", `/report/${encodeURIComponent(id)}`)));
|
|
335
|
+
const errors = reports.filter((report) => report && report.error);
|
|
336
|
+
if (errors.length) {
|
|
337
|
+
return { error: errors[0].error };
|
|
338
|
+
}
|
|
339
|
+
return combineReports(reports, ids);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (toolName === "audit_mcp_server" && !isAdminModeEnabled()) {
|
|
343
|
+
return { error: ACTIVE_SERVER_PROBING_DISABLED_MESSAGE };
|
|
188
344
|
}
|
|
189
345
|
|
|
190
346
|
const route = toolRoutes[toolName];
|
|
@@ -267,4 +423,12 @@ if (require.main === module) {
|
|
|
267
423
|
});
|
|
268
424
|
}
|
|
269
425
|
|
|
270
|
-
module.exports = {
|
|
426
|
+
module.exports = {
|
|
427
|
+
main,
|
|
428
|
+
runAuditTool,
|
|
429
|
+
testOnly: {
|
|
430
|
+
ACTIVE_SERVER_PROBING_DISABLED_MESSAGE,
|
|
431
|
+
combineReports,
|
|
432
|
+
toolDefinitions
|
|
433
|
+
}
|
|
434
|
+
};
|
package/mcp/server.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-audit-server",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Audit and remediate AI agent and MCP server security vulnerabilities, prompt injection risk, and data exfiltration paths.",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Audit and remediate AI agent and MCP server security vulnerabilities, prompt injection risk, and data exfiltration paths through a hosted audit backend.",
|
|
5
5
|
"command": "node",
|
|
6
6
|
"args": [
|
|
7
7
|
"mcp/index.js"
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ledd-mcp-audit-server",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "MCP server interface for AI agent and MCP security auditing — config analysis, prompt injection testing, tool probing, data flow tracing",
|
|
5
|
+
"mcpName": "io.github.joepangallo/mcp-audit-server",
|
|
5
6
|
"type": "commonjs",
|
|
6
7
|
"main": "index.js",
|
|
7
8
|
"bin": {
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
"cli.js",
|
|
36
37
|
"CHANGELOG.md",
|
|
37
38
|
"MIGRATION.md",
|
|
39
|
+
"server.json",
|
|
38
40
|
"mcp/",
|
|
39
41
|
"README.md",
|
|
40
42
|
"LICENSE"
|
|
@@ -42,6 +44,9 @@
|
|
|
42
44
|
"dependencies": {
|
|
43
45
|
"@modelcontextprotocol/sdk": "^1.17.0"
|
|
44
46
|
},
|
|
47
|
+
"overrides": {
|
|
48
|
+
"hono": "^4.12.7"
|
|
49
|
+
},
|
|
45
50
|
"engines": {
|
|
46
51
|
"node": ">=18"
|
|
47
52
|
},
|
package/server.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.joepangallo/mcp-audit-server",
|
|
4
|
+
"description": "MCP server interface for AI agent and MCP security auditing — config analysis, prompt injection testing, tool probing, data flow tracing",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/joepangallo/mcp-audit-server",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "2.0.2",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "ledd-mcp-audit-server",
|
|
14
|
+
"version": "2.0.2",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
18
|
+
"environmentVariables": [
|
|
19
|
+
{
|
|
20
|
+
"description": "API key for the managed hosted audit backend",
|
|
21
|
+
"isRequired": true,
|
|
22
|
+
"format": "string",
|
|
23
|
+
"isSecret": true,
|
|
24
|
+
"name": "AGENT_SECURITY_API_KEY"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"description": "Optional HTTPS API origin for self-hosted or private deployments",
|
|
28
|
+
"isRequired": false,
|
|
29
|
+
"format": "string",
|
|
30
|
+
"isSecret": false,
|
|
31
|
+
"name": "AGENT_SECURITY_BASE_URL"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|