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 +22 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +49 -0
- package/README.md +125 -0
- package/cli.js +346 -0
- package/index.js +50 -0
- package/mcp/index.js +270 -0
- package/mcp/server.json +47 -0
- package/package.json +49 -0
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 };
|
package/mcp/server.json
ADDED
|
@@ -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
|
+
}
|