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 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. For hosted deployments, set `AGENT_SECURITY_BASE_URL` to your HTTPS API origin.
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` | Audit API host |
94
- | `AGENT_SECURITY_PORT` | `3091` | Audit API port |
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
- " AGENT_SECURITY_BASE_URL Full audit API origin, e.g. https://audit.example.com",
31
- " AGENT_SECURITY_HOST Server host (default: 127.0.0.1)",
32
- " AGENT_SECURITY_PORT Server port (default: 3091)",
33
- " AGENT_SECURITY_API_KEY API key for remote access (optional)",
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 response = await fetch(`${BASE_URL}${pathname}`, {
48
- method,
49
- headers,
50
- body: payload ? JSON.stringify(payload) : undefined
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 [, , command, ...args] = process.argv;
166
- const jsonMode = process.argv.includes("--json");
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
- * By default it targets a local API on http://127.0.0.1:3091, but hosted
6
- * deployments should prefer AGENT_SECURITY_BASE_URL with an https:// origin.
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 PORT = Number.parseInt(process.env.AGENT_SECURITY_PORT || "", 10) || 3091;
15
- const HOST = process.env.AGENT_SECURITY_HOST || "127.0.0.1";
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
- function formatHostForUrl(host) {
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 "127.0.0.1";
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
- if (!/^https?:\/\//i.test(configuredBaseUrl)) {
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
- const host = typeof options.host === "string" ? options.host : HOST;
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: process.env.AGENT_SECURITY_BASE_URL,
46
- host: 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 = { PORT, HOST, BASE_URL, formatHostForUrl, resolveBaseUrl };
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 response = await fetch(`${AUDIT_BASE_URL}${apiPath}`, {
137
- method,
138
- headers,
139
- body: payload ? JSON.stringify(payload) : undefined,
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 results = await Promise.all(ids.map((id) => callAuditApi("GET", `/report/${encodeURIComponent(id)}`)));
187
- return { reports: results };
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 = { main, runAuditTool };
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.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.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
+ }