ledd-mcp-audit-server 2.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## 2.0.0 (2026-03-15)
4
+
5
+ ### Breaking Changes
6
+ - Scan engine moved to private API service. This package is now a thin MCP/CLI proxy.
7
+ - Published package renamed to `ledd-mcp-audit-server` to avoid npm namespace collisions while keeping the CLI command as `mcp-audit-server`.
8
+ - Removed `lib/` directory and all in-process scan modules.
9
+ - Requires access to a private audit API.
10
+
11
+ ### Added
12
+ - Tool spoofing detection (CWE-290) — duplicate tool names, namespace collision
13
+ - Rug pull detection (CWE-829) — unpinned packages, version drift
14
+ - Credential hygiene checks — inline secrets, missing rotation
15
+ - 9 MCP tools for comprehensive agent security auditing
16
+ - CLI with formatted output and --json mode
17
+ - Rate limiting on MCP server (30 req/min)
18
+ - `AGENT_SECURITY_BASE_URL` for hosted HTTPS backends
19
+
20
+ ### Removed
21
+ - All in-process scan modules (moved to a private backend)
22
+ - Direct dependencies on better-sqlite3, express, uuid
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ledd Consulting
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/MIGRATION.md ADDED
@@ -0,0 +1,49 @@
1
+ # Migration Guide
2
+
3
+ This package replaces the ambiguous `mcp-server-agent-security` name for the thin MCP/CLI proxy.
4
+
5
+ ## What Changed
6
+
7
+ - Old package name: `mcp-server-agent-security`
8
+ - New package name: `ledd-mcp-audit-server`
9
+ - Preferred CLI: `mcp-audit-server`
10
+
11
+ ## Upgrade
12
+
13
+ ```bash
14
+ npm uninstall mcp-server-agent-security
15
+ npm install ledd-mcp-audit-server
16
+ ```
17
+
18
+ If you launch the MCP proxy through `npx`, update your client config:
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "mcp-audit-server": {
24
+ "command": "npx",
25
+ "args": ["-y", "ledd-mcp-audit-server", "--mcp"]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Package Split
32
+
33
+ The old package name was overloaded across two different repos. The split is now:
34
+
35
+ - private audit engine: proprietary backend, rules, storage, and remediation logic
36
+ - `ledd-mcp-audit-server`: thin MCP/CLI proxy that forwards to that private audit API
37
+
38
+ Use `ledd-mcp-audit-server` when you want the public client package. It installs the `mcp-audit-server` CLI and should be configured against your hosted or licensed private audit API.
39
+
40
+ ## Publisher Checklist
41
+
42
+ 1. Publish this package under the new name: `npm publish`
43
+ 2. Deprecate the old package name:
44
+
45
+ ```bash
46
+ npm deprecate mcp-server-agent-security@"*" "Deprecated: use ledd-mcp-audit-server. The audit backend is now private and licensed separately."
47
+ ```
48
+
49
+ 3. Update the public README to point users to your hosted API or sales contact, not to a public engine package.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # mcp-audit-server
2
+
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
+
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.
6
+
7
+ 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
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install ledd-mcp-audit-server
13
+ ```
14
+
15
+ Install package: `ledd-mcp-audit-server`
16
+ CLI command after install: `mcp-audit-server`
17
+
18
+ The old package name `mcp-server-agent-security` is retired. See [MIGRATION.md](./MIGRATION.md) for upgrade steps and the deprecation plan.
19
+
20
+ ## Usage as MCP Server
21
+
22
+ Add to your MCP client configuration (Claude Desktop, Cursor, etc.):
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "mcp-audit-server": {
28
+ "command": "npx",
29
+ "args": ["-y", "ledd-mcp-audit-server", "--mcp"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ The server exposes 9 tools over stdio:
36
+
37
+ | Tool | Description |
38
+ |------|-------------|
39
+ | `audit_mcp_config` | Static analysis of MCP config JSON for privilege, auth, transport, and launch risks |
40
+ | `audit_mcp_server` | Active probing of a running MCP server over stdio (requires `AGENT_SECURITY_ADMIN_MODE=1`) |
41
+ | `audit_prompt_injection` | Tests a system prompt against a 30+ payload injection catalog |
42
+ | `audit_agent_dataflow` | Traces PII and secret exposure through an agent's tool pipeline |
43
+ | `scan_mcp_package` | Scans an npm MCP package for dependency vulnerabilities and dangerous patterns |
44
+ | `generate_report` | Combines multiple audit results into a composite report with executive summary |
45
+ | `fix_mcp_config` | Auto-remediates config issues: removes unsafe flags, upgrades transport, redacts secrets |
46
+ | `harden_system_prompt` | Appends injection-resistant guardrails to a system prompt |
47
+ | `generate_policy` | Generates an enforceable JSON security policy from an MCP config |
48
+
49
+ ## Usage as CLI
50
+
51
+ The CLI forwards commands to the private audit API.
52
+
53
+ ```bash
54
+ # Audit an MCP configuration file
55
+ mcp-audit-server scan-config ./claude_desktop_config.json
56
+
57
+ # Probe a live MCP server (requires AGENT_SECURITY_ADMIN_MODE=1)
58
+ mcp-audit-server scan-server npx -y @modelcontextprotocol/server-filesystem /tmp
59
+
60
+ # Scan an npm package for vulnerabilities
61
+ mcp-audit-server scan-package @modelcontextprotocol/server-shell
62
+
63
+ # Test a system prompt for injection vulnerabilities
64
+ mcp-audit-server scan-injection ./system-prompt.txt
65
+
66
+ # Trace data flows through an MCP config
67
+ mcp-audit-server scan-dataflow ./claude_desktop_config.json
68
+
69
+ # Auto-fix security issues in an MCP config
70
+ mcp-audit-server fix-config ./claude_desktop_config.json
71
+
72
+ # Harden a system prompt against injection
73
+ mcp-audit-server harden-prompt ./system-prompt.txt
74
+
75
+ # Generate a security policy from an MCP config
76
+ mcp-audit-server generate-policy ./claude_desktop_config.json
77
+
78
+ # Retrieve a previous audit report
79
+ mcp-audit-server report <audit-id>
80
+
81
+ # Output raw JSON instead of formatted tables
82
+ mcp-audit-server scan-config ./config.json --json
83
+
84
+ # Start in MCP stdio server mode
85
+ mcp-audit-server --mcp
86
+ ```
87
+
88
+ ## Environment Variables
89
+
90
+ | Variable | Default | Description |
91
+ |----------|---------|-------------|
92
+ | `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 |
96
+ | `AGENT_SECURITY_ADMIN_MODE` | (none) | Set to `1` to enable active server probing |
97
+
98
+ ## What It Detects
99
+
100
+ - **Tool spoofing** -- duplicate tool names, namespace collision (CWE-290)
101
+ - **Rug pull** -- unpinned packages, version drift (CWE-829)
102
+ - **Prompt injection** -- direct override, instruction hijacking, role-play escape, delimiter injection, encoding bypass, multilingual injection
103
+ - **Privilege escalation** -- overprivileged tools, shell execution without allowlists, unrestricted filesystem access
104
+ - **Data exfiltration** -- PII leakage through tool pipelines, outbound network paths
105
+ - **Insecure transport** -- missing TLS, plaintext credentials in config
106
+ - **Missing auth** -- unauthenticated MCP servers, missing API key requirements
107
+ - **Shell injection** -- arbitrary command execution via tool configurations
108
+ - **Path traversal** -- unrestricted filesystem scope in tool arguments
109
+ - **SQL injection** -- raw SQL patterns in tool definitions
110
+ - **Rate limiting** -- missing request throttling on exposed tools
111
+ - **Package vulnerabilities** -- known CVEs in npm MCP package dependencies
112
+ - **Credential exposure** -- inline secrets, missing rotation policies
113
+
114
+ ## Requirements
115
+
116
+ - 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.
118
+
119
+ ## License
120
+
121
+ MIT
122
+
123
+ ---
124
+
125
+ Built by [Ledd Consulting](https://leddconsulting.com)
package/cli.js ADDED
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { BASE_URL } = require("./index");
6
+
7
+ const API_KEY = process.env.AGENT_SECURITY_API_KEY || "";
8
+
9
+ function printUsage() {
10
+ process.stderr.write(
11
+ [
12
+ "Usage:",
13
+ " mcp-audit-server scan-config <file> Audit an MCP config file",
14
+ " mcp-audit-server scan-server <command> [args...] Probe a running MCP server",
15
+ " mcp-audit-server scan-package <name> Audit an npm package",
16
+ " mcp-audit-server scan-injection <file> Test a system prompt for injection vulnerabilities",
17
+ " mcp-audit-server scan-dataflow <file> Trace data flows through an MCP config",
18
+ " mcp-audit-server fix-config <file> Auto-fix security issues in an MCP config",
19
+ " mcp-audit-server harden-prompt <file> Harden a system prompt against injection",
20
+ " mcp-audit-server generate-policy <file> Generate a security policy from an MCP config",
21
+ " mcp-audit-server report <id> Retrieve an audit report by ID",
22
+ "",
23
+ "Flags:",
24
+ " --mcp Start the MCP stdio server (for use with MCP clients)",
25
+ " --help, -h Show this help message",
26
+ " --version, -v Show version number",
27
+ " --json Output raw JSON instead of formatted tables",
28
+ "",
29
+ "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)",
34
+ " AGENT_SECURITY_ADMIN_MODE Enable active server probing (set to \"1\")"
35
+ ].join("\n") + "\n"
36
+ );
37
+ }
38
+
39
+ async function callApi(method, pathname, payload) {
40
+ const headers = {
41
+ "content-type": "application/json"
42
+ };
43
+ if (API_KEY) {
44
+ headers["x-api-key"] = API_KEY;
45
+ }
46
+
47
+ const response = await fetch(`${BASE_URL}${pathname}`, {
48
+ method,
49
+ headers,
50
+ body: payload ? JSON.stringify(payload) : undefined
51
+ });
52
+
53
+ let body;
54
+ try {
55
+ body = await response.json();
56
+ } catch (parseError) {
57
+ throw new Error(`Request failed with status ${response.status} (non-JSON response).`);
58
+ }
59
+ if (!response.ok) {
60
+ throw new Error(body && body.error ? body.error : `Request failed with status ${response.status}`);
61
+ }
62
+
63
+ return body;
64
+ }
65
+
66
+ function formatReport(report) {
67
+ console.table([
68
+ {
69
+ id: report.id,
70
+ type: report.type || "",
71
+ target: report.target || "",
72
+ status: report.status,
73
+ score: report.score,
74
+ grade: report.grade
75
+ }
76
+ ]);
77
+
78
+ if (report.findingsSummary) {
79
+ console.table([report.findingsSummary]);
80
+ }
81
+
82
+ if (Array.isArray(report.findings) && report.findings.length) {
83
+ console.table(report.findings.map((finding) => ({
84
+ severity: finding.severity,
85
+ source: finding.source,
86
+ cwe: finding.cwe,
87
+ confidence: finding.confidence,
88
+ location: finding.location || "",
89
+ description: finding.description
90
+ })));
91
+ } else {
92
+ process.stdout.write("No findings.\n");
93
+ }
94
+ }
95
+
96
+ function formatFixConfig(result) {
97
+ process.stdout.write(`Original findings: ${result.original_findings}\n`);
98
+ process.stdout.write(`Changes applied: ${result.changes_applied}\n`);
99
+ process.stdout.write(`Remaining findings: ${result.remaining_findings}\n\n`);
100
+
101
+ if (Array.isArray(result.changes) && result.changes.length) {
102
+ console.table(result.changes.map((change) => ({
103
+ server: change.server,
104
+ action: change.action,
105
+ severity: change.severity,
106
+ detail: change.detail
107
+ })));
108
+ } else {
109
+ process.stdout.write("No changes needed.\n");
110
+ }
111
+
112
+ if (result.fixed_config) {
113
+ process.stdout.write("\nFixed config:\n");
114
+ process.stdout.write(JSON.stringify(result.fixed_config, null, 2) + "\n");
115
+ }
116
+ }
117
+
118
+ function formatHardenPrompt(result) {
119
+ process.stdout.write(`Action: ${result.action}\n`);
120
+ process.stdout.write(`Before score: ${result.before_score}\n`);
121
+ process.stdout.write(`After score: ${result.after_score}\n`);
122
+
123
+ if (result.improvement !== undefined) {
124
+ process.stdout.write(`Improvement: +${result.improvement}\n`);
125
+ }
126
+
127
+ if (Array.isArray(result.guardrails_added) && result.guardrails_added.length) {
128
+ process.stdout.write("\nGuardrails added:\n");
129
+ console.table(result.guardrails_added.map((g) => ({
130
+ control: g.control,
131
+ label: g.label
132
+ })));
133
+ }
134
+
135
+ if (result.hardened_prompt) {
136
+ process.stdout.write("\nHardened prompt:\n");
137
+ process.stdout.write(result.hardened_prompt + "\n");
138
+ }
139
+ }
140
+
141
+ function formatGeneratePolicy(result) {
142
+ process.stdout.write(`Rules generated: ${result.rule_count}\n`);
143
+ process.stdout.write(`Servers covered: ${result.servers_covered}\n`);
144
+
145
+ if (result.policy) {
146
+ if (Array.isArray(result.policy.rules) && result.policy.rules.length) {
147
+ console.table(result.policy.rules.map((rule) => ({
148
+ server: rule.server,
149
+ rule: rule.rule,
150
+ action: rule.action,
151
+ description: rule.description
152
+ })));
153
+ }
154
+
155
+ process.stdout.write("\nFull policy:\n");
156
+ process.stdout.write(JSON.stringify(result.policy, null, 2) + "\n");
157
+ }
158
+
159
+ if (result.usage) {
160
+ process.stdout.write("\n" + result.usage + "\n");
161
+ }
162
+ }
163
+
164
+ async function main() {
165
+ const [, , command, ...args] = process.argv;
166
+ const jsonMode = process.argv.includes("--json");
167
+
168
+ try {
169
+ if (command === "--help" || command === "-h") {
170
+ printUsage();
171
+ return;
172
+ }
173
+
174
+ if (command === "--version" || command === "-v") {
175
+ const pkg = require("./package.json");
176
+ process.stdout.write(pkg.version + "\n");
177
+ return;
178
+ }
179
+
180
+ if (command === "scan-config") {
181
+ const filePath = args[0];
182
+ if (!filePath) {
183
+ throw new Error("scan-config requires a file path.");
184
+ }
185
+
186
+ const absolutePath = path.resolve(process.cwd(), filePath);
187
+ const config = await fs.promises.readFile(absolutePath, "utf8");
188
+ const report = await callApi("POST", "/audit/config", { config });
189
+ if (jsonMode) {
190
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
191
+ } else {
192
+ formatReport(report);
193
+ }
194
+ return;
195
+ }
196
+
197
+ if (command === "scan-server") {
198
+ const targetCommand = args[0];
199
+ if (!targetCommand) {
200
+ throw new Error("scan-server requires a command.");
201
+ }
202
+
203
+ const report = await callApi("POST", "/audit/server", {
204
+ command: targetCommand,
205
+ args: args.slice(1)
206
+ });
207
+ if (jsonMode) {
208
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
209
+ } else {
210
+ formatReport(report);
211
+ }
212
+ return;
213
+ }
214
+
215
+ if (command === "scan-package") {
216
+ const packageName = args[0];
217
+ if (!packageName) {
218
+ throw new Error("scan-package requires a package name.");
219
+ }
220
+
221
+ const report = await callApi("POST", "/audit/package", {
222
+ package_name: packageName
223
+ });
224
+ if (jsonMode) {
225
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
226
+ } else {
227
+ formatReport(report);
228
+ }
229
+ return;
230
+ }
231
+
232
+ if (command === "scan-injection") {
233
+ const filePath = args[0];
234
+ if (!filePath) {
235
+ throw new Error("scan-injection requires a file path.");
236
+ }
237
+
238
+ const absolutePath = path.resolve(process.cwd(), filePath);
239
+ const systemPrompt = await fs.promises.readFile(absolutePath, "utf8");
240
+ const report = await callApi("POST", "/audit/injection", { system_prompt: systemPrompt });
241
+ if (jsonMode) {
242
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
243
+ } else {
244
+ formatReport(report);
245
+ }
246
+ return;
247
+ }
248
+
249
+ if (command === "scan-dataflow") {
250
+ const filePath = args[0];
251
+ if (!filePath) {
252
+ throw new Error("scan-dataflow requires a file path.");
253
+ }
254
+
255
+ const absolutePath = path.resolve(process.cwd(), filePath);
256
+ const mcpConfig = await fs.promises.readFile(absolutePath, "utf8");
257
+ const report = await callApi("POST", "/audit/dataflow", { mcp_config: mcpConfig });
258
+ if (jsonMode) {
259
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
260
+ } else {
261
+ formatReport(report);
262
+ }
263
+ return;
264
+ }
265
+
266
+ if (command === "fix-config") {
267
+ const filePath = args[0];
268
+ if (!filePath) {
269
+ throw new Error("fix-config requires a file path.");
270
+ }
271
+
272
+ const absolutePath = path.resolve(process.cwd(), filePath);
273
+ const config = await fs.promises.readFile(absolutePath, "utf8");
274
+ const result = await callApi("POST", "/fix/config", { config });
275
+ if (jsonMode) {
276
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
277
+ } else {
278
+ formatFixConfig(result.findings || result);
279
+ }
280
+ return;
281
+ }
282
+
283
+ if (command === "harden-prompt") {
284
+ const filePath = args[0];
285
+ if (!filePath) {
286
+ throw new Error("harden-prompt requires a file path.");
287
+ }
288
+
289
+ const absolutePath = path.resolve(process.cwd(), filePath);
290
+ const systemPrompt = await fs.promises.readFile(absolutePath, "utf8");
291
+ const result = await callApi("POST", "/fix/prompt", { system_prompt: systemPrompt });
292
+ if (jsonMode) {
293
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
294
+ } else {
295
+ formatHardenPrompt(result.findings || result);
296
+ }
297
+ return;
298
+ }
299
+
300
+ if (command === "generate-policy") {
301
+ const filePath = args[0];
302
+ if (!filePath) {
303
+ throw new Error("generate-policy requires a file path.");
304
+ }
305
+
306
+ const absolutePath = path.resolve(process.cwd(), filePath);
307
+ const mcpConfig = await fs.promises.readFile(absolutePath, "utf8");
308
+ const result = await callApi("POST", "/fix/policy", { mcp_config: mcpConfig });
309
+ if (jsonMode) {
310
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
311
+ } else {
312
+ formatGeneratePolicy(result.findings || result);
313
+ }
314
+ return;
315
+ }
316
+
317
+ if (command === "report") {
318
+ const reportId = args[0];
319
+ if (!reportId) {
320
+ throw new Error("report requires an audit id.");
321
+ }
322
+
323
+ const report = await callApi("GET", `/report/${encodeURIComponent(reportId)}`);
324
+ if (jsonMode) {
325
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
326
+ } else {
327
+ formatReport(report);
328
+ }
329
+ return;
330
+ }
331
+
332
+ printUsage();
333
+ process.exitCode = 1;
334
+ } catch (error) {
335
+ process.stderr.write(`${error.message}\n`);
336
+ process.exitCode = 1;
337
+ }
338
+ }
339
+
340
+ if (require.main === module) {
341
+ if (process.argv.includes("--mcp")) {
342
+ require("./mcp/index.js");
343
+ } else {
344
+ main();
345
+ }
346
+ }
package/index.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * mcp-audit-server — public entry point
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.
7
+ *
8
+ * Start the MCP server: node mcp/index.js
9
+ * Use the CLI: node cli.js scan-config <file>
10
+ */
11
+
12
+ const net = require("net");
13
+
14
+ const PORT = Number.parseInt(process.env.AGENT_SECURITY_PORT || "", 10) || 3091;
15
+ const HOST = process.env.AGENT_SECURITY_HOST || "127.0.0.1";
16
+
17
+ function formatHostForUrl(host) {
18
+ const value = String(host || "").trim();
19
+ if (!value) {
20
+ return "127.0.0.1";
21
+ }
22
+
23
+ if (value.startsWith("[") && value.endsWith("]")) {
24
+ return value;
25
+ }
26
+
27
+ return net.isIP(value) === 6 ? `[${value}]` : value;
28
+ }
29
+
30
+ function resolveBaseUrl(options = {}) {
31
+ const configuredBaseUrl = typeof options.baseUrl === "string" ? options.baseUrl.trim() : "";
32
+ if (configuredBaseUrl) {
33
+ if (!/^https?:\/\//i.test(configuredBaseUrl)) {
34
+ throw new Error("AGENT_SECURITY_BASE_URL must start with http:// or https://.");
35
+ }
36
+ return configuredBaseUrl.replace(/\/+$/, "");
37
+ }
38
+
39
+ const host = typeof options.host === "string" ? options.host : HOST;
40
+ const port = Number.isInteger(options.port) ? options.port : PORT;
41
+ return `http://${formatHostForUrl(host)}:${port}`;
42
+ }
43
+
44
+ const BASE_URL = resolveBaseUrl({
45
+ baseUrl: process.env.AGENT_SECURITY_BASE_URL,
46
+ host: HOST,
47
+ port: PORT
48
+ });
49
+
50
+ module.exports = { PORT, HOST, BASE_URL, formatHostForUrl, resolveBaseUrl };
package/mcp/index.js ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * MCP server for agent-security — thin proxy to the private audit API.
3
+ *
4
+ * All scan logic runs on a private audit API.
5
+ * This MCP server only exposes tool definitions and forwards requests.
6
+ */
7
+
8
+ const AUDIT_API_KEY = process.env.AGENT_SECURITY_API_KEY || "";
9
+ const { BASE_URL: AUDIT_BASE_URL } = require("../index");
10
+ const { version: APP_VERSION } = require("../package.json");
11
+
12
+ const MCP_MAX_REQUESTS_PER_MINUTE = 30;
13
+ const MCP_WINDOW_MS = 60_000;
14
+ let mcpRequestCount = 0;
15
+ let mcpWindowStart = Date.now();
16
+
17
+ const toolDefinitions = [
18
+ {
19
+ name: "audit_mcp_config",
20
+ description: "Perform static analysis on raw MCP config JSON and identify privilege, auth, transport, and launch risks.",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: { config: { type: "string", description: "Raw MCP config JSON." } },
24
+ required: ["config"]
25
+ }
26
+ },
27
+ {
28
+ name: "audit_mcp_server",
29
+ description: "Launch a target MCP server over stdio, enumerate tools, and run active security probes against its exposed tools. Requires AGENT_SECURITY_ADMIN_MODE=1.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ command: { type: "string" },
34
+ args: { type: "array", items: { type: "string" } },
35
+ env: { type: "object", additionalProperties: { type: "string" } }
36
+ },
37
+ required: ["command"]
38
+ }
39
+ },
40
+ {
41
+ name: "audit_prompt_injection",
42
+ description: "Perform a static prompt-hardening review against a 30+ payload prompt-injection catalog.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ system_prompt: { type: "string" },
47
+ tools: { type: "array", items: { type: "string" } }
48
+ },
49
+ required: ["system_prompt"]
50
+ }
51
+ },
52
+ {
53
+ name: "audit_agent_dataflow",
54
+ description: "Infer tagged-data exposure and exfiltration paths from MCP config and observed tool capabilities.",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ mcp_config: { type: "string" },
59
+ test_pii: { type: "string" }
60
+ },
61
+ required: ["mcp_config"]
62
+ }
63
+ },
64
+ {
65
+ name: "scan_mcp_package",
66
+ description: "Scan an npm MCP package for dependency vulnerabilities, dangerous patterns, and permission issues.",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: { package_name: { type: "string" } },
70
+ required: ["package_name"]
71
+ }
72
+ },
73
+ {
74
+ name: "generate_report",
75
+ description: "Combine multiple stored audit jobs into a composite report with deduplicated findings and an executive summary.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: { audit_ids: { type: "array", items: { type: "string" } } },
79
+ required: ["audit_ids"]
80
+ }
81
+ },
82
+ {
83
+ name: "fix_mcp_config",
84
+ description: "Auto-remediate security issues in an MCP config: remove unsafe flags, strip shell wrappers, upgrade transport to TLS, redact inline secrets, add auth placeholders, and constrain filesystem scope.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: { config: { type: "string", description: "Raw MCP config JSON to fix." } },
88
+ required: ["config"]
89
+ }
90
+ },
91
+ {
92
+ name: "harden_system_prompt",
93
+ description: "Analyze a system prompt for injection vulnerabilities and return a hardened version with security guardrails appended.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ system_prompt: { type: "string", description: "The system prompt to harden." },
98
+ tools: { type: "array", items: { type: "string" }, description: "Tool names available to the agent." }
99
+ },
100
+ required: ["system_prompt"]
101
+ }
102
+ },
103
+ {
104
+ name: "generate_policy",
105
+ description: "Generate a JSON security policy from an MCP config that can be enforced by a proxy or middleware.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ mcp_config: { type: "string", description: "Raw MCP config JSON." },
110
+ allowed_destinations: { type: "array", items: { type: "string" } },
111
+ allowed_paths: { type: "array", items: { type: "string" } }
112
+ },
113
+ required: ["mcp_config"]
114
+ }
115
+ }
116
+ ];
117
+
118
+ // Route table: MCP tool name → private API endpoint
119
+ const toolRoutes = {
120
+ audit_mcp_config: { method: "POST", path: "/audit/config", body: (a) => ({ config: a.config }) },
121
+ audit_mcp_server: { method: "POST", path: "/audit/server", body: (a) => ({ command: a.command, args: a.args, env: a.env }) },
122
+ audit_prompt_injection: { method: "POST", path: "/audit/injection", body: (a) => ({ system_prompt: a.system_prompt, tools: a.tools }) },
123
+ audit_agent_dataflow: { method: "POST", path: "/audit/dataflow", body: (a) => ({ mcp_config: a.mcp_config, test_pii: a.test_pii }) },
124
+ scan_mcp_package: { method: "POST", path: "/audit/package", body: (a) => ({ package_name: a.package_name }) },
125
+ fix_mcp_config: { method: "POST", path: "/fix/config", body: (a) => ({ config: a.config }) },
126
+ harden_system_prompt: { method: "POST", path: "/fix/prompt", body: (a) => ({ system_prompt: a.system_prompt, tools: a.tools }) },
127
+ generate_policy: { method: "POST", path: "/fix/policy", body: (a) => ({ mcp_config: a.mcp_config, allowed_destinations: a.allowed_destinations, allowed_paths: a.allowed_paths }) },
128
+ };
129
+
130
+ async function callAuditApi(method, apiPath, payload) {
131
+ const headers = { "Content-Type": "application/json" };
132
+ if (AUDIT_API_KEY) {
133
+ headers["x-api-key"] = AUDIT_API_KEY;
134
+ }
135
+
136
+ const response = await fetch(`${AUDIT_BASE_URL}${apiPath}`, {
137
+ method,
138
+ headers,
139
+ body: payload ? JSON.stringify(payload) : undefined,
140
+ });
141
+
142
+ const text = await response.text();
143
+ let body;
144
+ try {
145
+ body = JSON.parse(text);
146
+ } catch {
147
+ return { error: `Audit API returned non-JSON (status ${response.status}): ${text.slice(0, 200)}` };
148
+ }
149
+
150
+ if (!response.ok) {
151
+ return { error: body.error || `Audit API returned status ${response.status}` };
152
+ }
153
+
154
+ return body;
155
+ }
156
+
157
+ function checkMcpRateLimit() {
158
+ const now = Date.now();
159
+ if (now - mcpWindowStart > MCP_WINDOW_MS) {
160
+ mcpRequestCount = 0;
161
+ mcpWindowStart = now;
162
+ }
163
+ mcpRequestCount++;
164
+ return mcpRequestCount <= MCP_MAX_REQUESTS_PER_MINUTE;
165
+ }
166
+
167
+ async function runAuditTool(toolName, args) {
168
+ if (!checkMcpRateLimit()) {
169
+ return { error: "Rate limit exceeded. Try again later." };
170
+ }
171
+
172
+ const safeArgs = args && typeof args === "object" && !Array.isArray(args) ? args : {};
173
+
174
+ // generate_report: fetch individual reports by ID
175
+ if (toolName === "generate_report") {
176
+ const ids = Array.isArray(safeArgs.audit_ids) ? safeArgs.audit_ids.map(String) : [];
177
+ if (ids.length === 0) {
178
+ return { error: "audit_ids must be a non-empty array." };
179
+ }
180
+ if (ids.length > 25) {
181
+ return { error: "audit_ids must contain at most 25 entries." };
182
+ }
183
+ if (ids.length === 1) {
184
+ return callAuditApi("GET", `/report/${encodeURIComponent(ids[0])}`);
185
+ }
186
+ const results = await Promise.all(ids.map((id) => callAuditApi("GET", `/report/${encodeURIComponent(id)}`)));
187
+ return { reports: results };
188
+ }
189
+
190
+ const route = toolRoutes[toolName];
191
+ if (!route) {
192
+ return { error: `Unknown tool: ${toolName}` };
193
+ }
194
+
195
+ return callAuditApi(route.method, route.path, route.body(safeArgs));
196
+ }
197
+
198
+ function importFirst(candidates) {
199
+ for (const id of candidates) {
200
+ try {
201
+ return require(id);
202
+ } catch {}
203
+ }
204
+ throw new Error(`Could not import any of: ${candidates.join(", ")}`);
205
+ }
206
+
207
+ async function main() {
208
+ const serverModule = importFirst([
209
+ "@modelcontextprotocol/sdk/server/index.js",
210
+ "@modelcontextprotocol/sdk/dist/esm/server/index.js",
211
+ "@modelcontextprotocol/sdk/dist/server/index.js",
212
+ ]);
213
+ const stdioModule = importFirst([
214
+ "@modelcontextprotocol/sdk/server/stdio.js",
215
+ "@modelcontextprotocol/sdk/dist/esm/server/stdio.js",
216
+ "@modelcontextprotocol/sdk/dist/server/stdio.js",
217
+ ]);
218
+ const typesModule = importFirst([
219
+ "@modelcontextprotocol/sdk/types.js",
220
+ "@modelcontextprotocol/sdk/dist/esm/types.js",
221
+ "@modelcontextprotocol/sdk/dist/types.js",
222
+ ]);
223
+
224
+ const Server = serverModule.Server || (serverModule.default && serverModule.default.Server);
225
+ const StdioServerTransport = stdioModule.StdioServerTransport || (stdioModule.default && stdioModule.default.StdioServerTransport);
226
+ const { ListToolsRequestSchema, CallToolRequestSchema } = typesModule;
227
+
228
+ if (!Server || !StdioServerTransport || !ListToolsRequestSchema || !CallToolRequestSchema) {
229
+ throw new Error("Unable to load the MCP server SDK classes.");
230
+ }
231
+
232
+ const server = new Server(
233
+ { name: "mcp-audit-server", version: APP_VERSION || "0.0.0" },
234
+ { capabilities: { tools: {} } }
235
+ );
236
+
237
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
238
+ tools: toolDefinitions,
239
+ }));
240
+
241
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
242
+ try {
243
+ const toolName = request?.params?.name || "";
244
+ const args = request?.params?.arguments || {};
245
+ const result = await runAuditTool(toolName, args);
246
+ const isToolError = Boolean(result?.error);
247
+ return {
248
+ ...(isToolError ? { isError: true } : {}),
249
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
250
+ };
251
+ } catch (error) {
252
+ return {
253
+ isError: true,
254
+ content: [{ type: "text", text: JSON.stringify({ error: error.message }, null, 2) }],
255
+ };
256
+ }
257
+ });
258
+
259
+ const transport = new StdioServerTransport();
260
+ await server.connect(transport);
261
+ }
262
+
263
+ if (require.main === module) {
264
+ main().catch((error) => {
265
+ process.stderr.write(`${error.stack || error.message}\n`);
266
+ process.exit(1);
267
+ });
268
+ }
269
+
270
+ module.exports = { main, runAuditTool };
@@ -0,0 +1,47 @@
1
+ {
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.",
5
+ "command": "node",
6
+ "args": [
7
+ "mcp/index.js"
8
+ ],
9
+ "tools": [
10
+ {
11
+ "name": "audit_mcp_config",
12
+ "description": "Static analysis of raw MCP config JSON."
13
+ },
14
+ {
15
+ "name": "audit_mcp_server",
16
+ "description": "Active probing of a running MCP server over stdio."
17
+ },
18
+ {
19
+ "name": "audit_prompt_injection",
20
+ "description": "Prompt-injection resistance analysis for a system prompt and tool set."
21
+ },
22
+ {
23
+ "name": "audit_agent_dataflow",
24
+ "description": "Capability-based PII and exfiltration path tracing."
25
+ },
26
+ {
27
+ "name": "scan_mcp_package",
28
+ "description": "npm package scan for dependencies, dangerous patterns, and permission issues."
29
+ },
30
+ {
31
+ "name": "generate_report",
32
+ "description": "Combine multiple stored audits into a single executive report."
33
+ },
34
+ {
35
+ "name": "fix_mcp_config",
36
+ "description": "Auto-remediate MCP config security issues and return a hardened config."
37
+ },
38
+ {
39
+ "name": "harden_system_prompt",
40
+ "description": "Harden a system prompt with injection-resistant guardrails."
41
+ },
42
+ {
43
+ "name": "generate_policy",
44
+ "description": "Generate an enforceable JSON security policy from MCP config."
45
+ }
46
+ ]
47
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "ledd-mcp-audit-server",
3
+ "version": "2.0.0",
4
+ "description": "MCP server interface for AI agent and MCP security auditing — config analysis, prompt injection testing, tool probing, data flow tracing",
5
+ "type": "commonjs",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "mcp-audit-server": "cli.js"
9
+ },
10
+ "scripts": {
11
+ "mcp:start": "node mcp/index.js",
12
+ "mcp": "node mcp/index.js",
13
+ "test": "node --test test/*.test.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "security",
18
+ "audit",
19
+ "ai-agent",
20
+ "prompt-injection",
21
+ "mcp-server",
22
+ "llm-security"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/joepangallo/mcp-audit-server.git"
27
+ },
28
+ "homepage": "https://github.com/joepangallo/mcp-audit-server#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/joepangallo/mcp-audit-server/issues"
31
+ },
32
+ "author": "Ledd Consulting <leddconsulting@gmail.com>",
33
+ "files": [
34
+ "index.js",
35
+ "cli.js",
36
+ "CHANGELOG.md",
37
+ "MIGRATION.md",
38
+ "mcp/",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.17.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=18"
47
+ },
48
+ "license": "MIT"
49
+ }