agent-mcp-guard 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,39 +1,26 @@
1
+ <p align="center">
2
+ <img src="site/assets/readme-hero.svg" alt="mcp-guard scan report hero" width="100%">
3
+ </p>
4
+
1
5
  # mcp-guard
2
6
 
3
- Open-source CLI scanner for risky MCP server and AI agent tool configuration.
7
+ Local-first security scanning for MCP and AI agent tool configs.
4
8
 
5
- `mcp-guard` helps developers review MCP configs before giving AI agents access to files, shells, credentials, SaaS tools, or production systems.
9
+ `mcp-guard` helps teams review what their AI agents can execute before those agents touch local files, shells, credentials, SaaS accounts, or production systems.
6
10
 
7
- ## What It Detects
11
+ Website: [chaoyue0307.github.io/mcp-guard](https://chaoyue0307.github.io/mcp-guard/)
8
12
 
9
- - Shell wrappers and inline scripts.
10
- - `node -e`, `python -c`, and other interpreter eval modes.
11
- - Remote package runners such as `npx`, `uvx`, `bunx`, and `pnpm dlx`.
12
- - Unpinned MCP server package versions.
13
- - Secret-like environment variables and headers.
14
- - Broad filesystem access such as `/`, home, Desktop, Documents, or Downloads.
15
- - Remote MCP server URLs.
16
- - Dangerous command patterns such as `rm -rf`, `sudo`, `chmod 777`, and curl pipe to shell.
13
+ <p>
14
+ <a href="https://www.npmjs.com/package/agent-mcp-guard"><img alt="npm version" src="https://img.shields.io/npm/v/agent-mcp-guard?color=0f766e"></a>
15
+ <a href="https://github.com/ChaoYue0307/mcp-guard/actions"><img alt="CI" src="https://github.com/ChaoYue0307/mcp-guard/actions/workflows/ci.yml/badge.svg"></a>
16
+ <a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-111827"></a>
17
+ <a href="https://github.com/ChaoYue0307/mcp-guard/releases/tag/v0.2.0"><img alt="Release" src="https://img.shields.io/github/v/release/ChaoYue0307/mcp-guard?color=7c2d12"></a>
18
+ </p>
17
19
 
18
20
  ## Install
19
21
 
20
- For local development from this repo:
21
-
22
- ```bash
23
- npm install -g .
24
- ```
25
-
26
- After npm publication:
27
-
28
22
  ```bash
29
23
  npm install -g agent-mcp-guard
30
- ```
31
-
32
- ## Usage
33
-
34
- Scan common Claude Desktop, Cursor, and project MCP config locations:
35
-
36
- ```bash
37
24
  mcp-guard scan
38
25
  ```
39
26
 
@@ -49,15 +36,57 @@ Generate a Markdown report:
49
36
  mcp-guard scan --format markdown --output mcp-guard-report.md
50
37
  ```
51
38
 
52
- Use in CI and fail when high-risk findings are present:
39
+ Generate an HTML report:
40
+
41
+ ```bash
42
+ mcp-guard scan --format html --output mcp-guard-report.html
43
+ ```
44
+
45
+ Use in CI:
53
46
 
54
47
  ```bash
55
48
  mcp-guard scan --config .mcp.json --fail-on high
56
49
  ```
57
50
 
58
- ## Supported Config Shape
51
+ Use the GitHub Action:
52
+
53
+ ```yaml
54
+ - uses: ChaoYue0307/mcp-guard@v0.2.0
55
+ with:
56
+ fail-on: high
57
+ ```
58
+
59
+ ## What It Finds
60
+
61
+ | Risk | Why it matters |
62
+ | --- | --- |
63
+ | Shell wrappers and inline scripts | Agent startup can become arbitrary code execution. |
64
+ | `npx`, `uvx`, `bunx`, `pnpm dlx` | Remote package execution expands supply-chain risk. |
65
+ | Unpinned packages | A trusted MCP server can change underneath you. |
66
+ | Secret-like env vars and headers | Long-lived tokens leak into tool runtimes and reports. |
67
+ | Broad filesystem access | Home, root, Desktop, Documents, and Downloads are high-blast-radius paths. |
68
+ | Remote MCP URLs | Data may leave the local trust boundary. |
69
+ | Dangerous command patterns | `rm -rf`, `sudo`, `chmod 777`, and curl-pipe-shell should block review. |
70
+
71
+ ## Example Output
72
+
73
+ ```text
74
+ mcp-guard scan report
75
+ Scanned files: 1
76
+ MCP servers: 3
77
+ Findings: 9
78
+ Risk score: 98
79
+ Critical: 2 High: 5 Medium: 2 Low: 0
80
+
81
+ - [CRITICAL] MCP010 Shell command executes inline script
82
+ - [HIGH] MCP021 Remote MCP package is not version pinned
83
+ - [HIGH] MCP030 Secret-like environment variable is exposed to MCP server
84
+ - [HIGH] MCP041 MCP server argument grants broad filesystem access
85
+ ```
86
+
87
+ See the full sample report: [examples/sample-report.md](examples/sample-report.md)
59
88
 
60
- `mcp-guard` supports the common MCP config shape used by Claude Desktop, Cursor, and many project configs:
89
+ ## Supported Config Shape
61
90
 
62
91
  ```json
63
92
  {
@@ -74,45 +103,49 @@ mcp-guard scan --config .mcp.json --fail-on high
74
103
  }
75
104
  ```
76
105
 
77
- It also accepts `servers` as an alternative top-level key.
106
+ `mcp-guard` supports the common `mcpServers` shape used by Claude Desktop, Cursor, and project-level MCP configs. It also accepts `servers` as an alternative top-level key.
78
107
 
79
- ## Example
80
-
81
- ```bash
82
- npm run scan:example
83
- ```
108
+ ## Why Local-First
84
109
 
85
- This scans `examples/unsafe-claude_desktop_config.json` and writes `examples/sample-report.md`.
110
+ MCP configs often contain sensitive local paths, internal hostnames, tokens, and workflow details. `mcp-guard` runs locally by default:
86
111
 
87
- ## Exit Codes
112
+ - no config upload;
113
+ - no external API call;
114
+ - secret-like values redacted in reports;
115
+ - text, Markdown, HTML, and JSON output for local review and CI.
88
116
 
89
- - `0`: scan completed and did not hit the fail threshold.
90
- - `1`: CLI usage or runtime error.
91
- - `2`: finding severity met `--fail-on` threshold.
117
+ ## Commercial Support
92
118
 
93
- ## Privacy
119
+ Need help reviewing a real AI agent or MCP setup?
94
120
 
95
- `mcp-guard` is local-first:
121
+ I offer private **AI Agent/MCP Security Audits** covering server inventory, risky startup commands, secret exposure, filesystem scope, remote MCP endpoints, and remediation planning.
96
122
 
97
- - It does not upload configs.
98
- - It does not call external APIs.
99
- - It redacts secret-like values in reports by default.
123
+ Contact: [hechaoyue0307@gmail.com](mailto:hechaoyue0307@gmail.com)
100
124
 
101
- MCP configs and reports can still contain sensitive paths, hostnames, and configuration details. Review before sharing.
125
+ Service details: [docs/paid-audit.md](docs/paid-audit.md)
102
126
 
103
127
  ## Documentation
104
128
 
105
129
  - [Rule reference](docs/rules.md)
130
+ - [GitHub Action](docs/github-action.md)
106
131
  - [Privacy and security](docs/privacy-and-security.md)
107
- - [Paid audit service](docs/paid-audit.md)
132
+ - [Roadmap](docs/roadmap.md)
133
+ - [Business playbook](docs/business-playbook.md)
108
134
  - [Launch checklist](docs/launch-checklist.md)
109
135
  - [Operator runbook](docs/operator-runbook.md)
110
136
 
111
- ## Commercial Support
137
+ ## Exit Codes
112
138
 
113
- Need a private AI Agent/MCP security audit?
139
+ - `0`: scan completed and did not hit the fail threshold.
140
+ - `1`: CLI usage or runtime error.
141
+ - `2`: finding severity met `--fail-on` threshold.
142
+
143
+ ## Development
114
144
 
115
- The first paid service is a focused review of your MCP and agent tool setup: inventory, risk report, remediation checklist, and a hardening call. See [docs/paid-audit.md](docs/paid-audit.md).
145
+ ```bash
146
+ npm test
147
+ npm run release:check
148
+ ```
116
149
 
117
150
  ## License
118
151
 
package/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ `mcp-guard` is early-stage software. Security fixes target the latest npm version.
6
+
7
+ ## Reporting A Vulnerability
8
+
9
+ Please do not open a public issue for a vulnerability that could expose users.
10
+
11
+ Email: [hechaoyue0307@gmail.com](mailto:hechaoyue0307@gmail.com)
12
+
13
+ Include:
14
+
15
+ - affected version;
16
+ - operating system;
17
+ - config sample with secrets removed;
18
+ - reproduction steps;
19
+ - expected and actual behavior;
20
+ - potential impact.
21
+
22
+ I will acknowledge valid reports as quickly as practical and coordinate disclosure before publishing details.
23
+
24
+ ## Scope
25
+
26
+ In scope:
27
+
28
+ - secret redaction failures;
29
+ - unexpected config upload or network access;
30
+ - incorrect handling of local files;
31
+ - CLI behavior that hides high-risk findings;
32
+ - supply-chain or package publishing issues.
33
+
34
+ Out of scope:
35
+
36
+ - generic MCP server vulnerabilities unrelated to `mcp-guard`;
37
+ - findings that require already-compromised local admin access;
38
+ - social engineering against maintainers;
39
+ - spam or automated low-signal reports.
40
+
package/action.yml ADDED
@@ -0,0 +1,111 @@
1
+ name: mcp-guard
2
+ description: Scan MCP and AI agent tool configuration for risky commands, secrets, and broad permissions.
3
+ author: mcp-guard
4
+
5
+ branding:
6
+ icon: shield
7
+ color: green
8
+
9
+ inputs:
10
+ config:
11
+ description: Optional MCP config path to scan. Leave empty to scan default project and user config locations.
12
+ required: false
13
+ default: ""
14
+ fail-on:
15
+ description: Fail the workflow when a finding is at least this severity. Use critical, high, medium, low, or none.
16
+ required: false
17
+ default: high
18
+ output-dir:
19
+ description: Directory where reports will be written.
20
+ required: false
21
+ default: mcp-guard-report
22
+ package-version:
23
+ description: npm package version to install.
24
+ required: false
25
+ default: latest
26
+ upload-artifact:
27
+ description: Upload generated reports as a workflow artifact.
28
+ required: false
29
+ default: "true"
30
+ artifact-name:
31
+ description: Artifact name for generated reports.
32
+ required: false
33
+ default: mcp-guard-report
34
+
35
+ outputs:
36
+ markdown-report:
37
+ description: Path to the generated Markdown report.
38
+ value: ${{ steps.reports.outputs.markdown-report }}
39
+ html-report:
40
+ description: Path to the generated HTML report.
41
+ value: ${{ steps.reports.outputs.html-report }}
42
+ json-report:
43
+ description: Path to the generated JSON report.
44
+ value: ${{ steps.reports.outputs.json-report }}
45
+ exit-code:
46
+ description: mcp-guard threshold exit code.
47
+ value: ${{ steps.reports.outputs.exit-code }}
48
+
49
+ runs:
50
+ using: composite
51
+ steps:
52
+ - name: Set up Node.js
53
+ uses: actions/setup-node@v4
54
+ with:
55
+ node-version: "20"
56
+
57
+ - name: Install mcp-guard
58
+ shell: bash
59
+ env:
60
+ MCP_GUARD_PACKAGE_VERSION: ${{ inputs.package-version }}
61
+ run: npm install --global "agent-mcp-guard@${MCP_GUARD_PACKAGE_VERSION}"
62
+
63
+ - name: Generate reports
64
+ id: reports
65
+ shell: bash
66
+ env:
67
+ MCP_GUARD_CONFIG: ${{ inputs.config }}
68
+ MCP_GUARD_FAIL_ON: ${{ inputs.fail-on }}
69
+ MCP_GUARD_OUTPUT_DIR: ${{ inputs.output-dir }}
70
+ run: |
71
+ set -euo pipefail
72
+
73
+ mkdir -p "${MCP_GUARD_OUTPUT_DIR}"
74
+
75
+ scan_args=()
76
+ if [ -n "${MCP_GUARD_CONFIG}" ]; then
77
+ scan_args+=(--config "${MCP_GUARD_CONFIG}")
78
+ fi
79
+
80
+ markdown_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.md"
81
+ html_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.html"
82
+ json_report="${MCP_GUARD_OUTPUT_DIR}/mcp-guard-report.json"
83
+
84
+ mcp-guard scan "${scan_args[@]}" --format markdown --output "${markdown_report}" --fail-on none
85
+ mcp-guard scan "${scan_args[@]}" --format html --output "${html_report}" --fail-on none
86
+ mcp-guard scan "${scan_args[@]}" --format json --output "${json_report}" --fail-on none
87
+
88
+ set +e
89
+ mcp-guard scan "${scan_args[@]}" --fail-on "${MCP_GUARD_FAIL_ON}"
90
+ status="$?"
91
+ set -e
92
+
93
+ {
94
+ echo "markdown-report=${markdown_report}"
95
+ echo "html-report=${html_report}"
96
+ echo "json-report=${json_report}"
97
+ echo "exit-code=${status}"
98
+ } >> "${GITHUB_OUTPUT}"
99
+
100
+ - name: Upload report artifact
101
+ if: ${{ always() && inputs.upload-artifact == 'true' }}
102
+ uses: actions/upload-artifact@v4
103
+ with:
104
+ name: ${{ inputs.artifact-name }}
105
+ path: ${{ inputs.output-dir }}
106
+
107
+ - name: Enforce severity threshold
108
+ shell: bash
109
+ env:
110
+ MCP_GUARD_EXIT_CODE: ${{ steps.reports.outputs.exit-code }}
111
+ run: exit "${MCP_GUARD_EXIT_CODE}"
@@ -0,0 +1,64 @@
1
+ # Business Playbook
2
+
3
+ ## Positioning
4
+
5
+ `mcp-guard` is the local-first security scanner for teams adopting AI agents and MCP servers.
6
+
7
+ The business is not the open-source CLI alone. The CLI creates trust and distribution. Revenue comes from private audits, remediation, and eventually team workflows.
8
+
9
+ ## First Paid Offer
10
+
11
+ AI Agent/MCP Security Audit.
12
+
13
+ Deliverables:
14
+
15
+ - MCP server inventory;
16
+ - `mcp-guard` Markdown, HTML, and JSON scan reports;
17
+ - manual review of high-risk findings;
18
+ - prioritized remediation plan;
19
+ - optional GitHub Action setup for continuous scans;
20
+ - 60-minute hardening call;
21
+ - optional PR with safer config changes.
22
+
23
+ ## Pricing
24
+
25
+ | Customer | Price |
26
+ | --- | ---: |
27
+ | Solo founder / indie team | USD 300-800 |
28
+ | Small startup | USD 1,000-3,000 |
29
+ | Funded team / private deployment pilot | USD 3,000-8,000 |
30
+
31
+ ## Outreach Copy
32
+
33
+ ```text
34
+ I built mcp-guard, an open-source local scanner for MCP and AI agent tool configs.
35
+
36
+ It checks for risky shell access, unpinned npx packages, broad filesystem permissions, exposed secrets, and remote MCP servers.
37
+
38
+ I am doing a few early MCP security audits for teams using Claude, Cursor, Codex, or MCP in real workflows. If you send a redacted config or run the CLI locally, I can help interpret the report and suggest hardening steps.
39
+ ```
40
+
41
+ ## First 20 Targets
42
+
43
+ - MCP server authors.
44
+ - AI automation agencies.
45
+ - Devtool startups using MCP.
46
+ - Teams publishing agent demos with real tool access.
47
+ - Founders discussing Cursor, Claude Code, Codex, or MCP on GitHub, X, LinkedIn, Hacker News, or Discord.
48
+
49
+ ## Validation Signals
50
+
51
+ Strong:
52
+
53
+ - user shares real redacted config;
54
+ - user asks for CI integration;
55
+ - user asks whether a finding is exploitable;
56
+ - user pays for remediation;
57
+ - team asks for monthly scanning.
58
+
59
+ Weak:
60
+
61
+ - stars without config samples;
62
+ - vague security interest;
63
+ - requests for a full dashboard before any audit;
64
+ - only free users with toy configs.
@@ -0,0 +1,68 @@
1
+ # GitHub Action
2
+
3
+ Use the `mcp-guard` action to scan MCP and AI agent tool configuration in pull requests and CI.
4
+
5
+ The action installs the published npm package, generates Markdown, HTML, and JSON reports, uploads them as a workflow artifact, then fails the job when findings meet your selected severity threshold.
6
+
7
+ ## Basic Workflow
8
+
9
+ ```yaml
10
+ name: mcp-guard
11
+
12
+ on:
13
+ pull_request:
14
+ push:
15
+ branches: [main]
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ scan:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: ChaoYue0307/mcp-guard@v0.2.0
26
+ with:
27
+ fail-on: high
28
+ ```
29
+
30
+ ## Scan a Specific Config
31
+
32
+ ```yaml
33
+ - uses: ChaoYue0307/mcp-guard@v0.2.0
34
+ with:
35
+ config: .mcp.json
36
+ fail-on: medium
37
+ ```
38
+
39
+ ## Pin the npm Package
40
+
41
+ The action defaults to `agent-mcp-guard@latest`. Pin it when you want deterministic CI behavior:
42
+
43
+ ```yaml
44
+ - uses: ChaoYue0307/mcp-guard@v0.2.0
45
+ with:
46
+ package-version: 0.2.0
47
+ fail-on: high
48
+ ```
49
+
50
+ ## Inputs
51
+
52
+ | Input | Default | Description |
53
+ | --- | --- | --- |
54
+ | `config` | empty | Optional MCP config path. Empty scans default project and user config locations. |
55
+ | `fail-on` | `high` | Fails the job for `critical`, `high`, `medium`, or `low` findings. Use `none` for report-only mode. |
56
+ | `output-dir` | `mcp-guard-report` | Directory for generated reports. |
57
+ | `package-version` | `latest` | npm package version to install. |
58
+ | `upload-artifact` | `true` | Uploads generated reports as a workflow artifact. |
59
+ | `artifact-name` | `mcp-guard-report` | Name of the uploaded artifact. |
60
+
61
+ ## Outputs
62
+
63
+ | Output | Description |
64
+ | --- | --- |
65
+ | `markdown-report` | Path to the generated Markdown report. |
66
+ | `html-report` | Path to the generated HTML report. |
67
+ | `json-report` | Path to the generated JSON report. |
68
+ | `exit-code` | `0` when below threshold, `2` when findings met the threshold. |
@@ -0,0 +1,33 @@
1
+ # Product Roadmap
2
+
3
+ `mcp-guard` should stay narrow: local-first MCP and AI agent tool security that produces actionable reports.
4
+
5
+ ## Now
6
+
7
+ - CLI config scanning.
8
+ - Text, Markdown, HTML, and redacted JSON output.
9
+ - Rules for shell wrappers, remote package runners, unpinned packages, broad filesystem access, secret-like env vars/headers, and remote MCP URLs.
10
+ - CI usage with `--fail-on`.
11
+ - GitHub Action wrapper that uploads Markdown, HTML, and JSON reports as artifacts.
12
+
13
+ ## Next
14
+
15
+ 1. More MCP client discovery paths.
16
+ 2. Rule packs mapped to MCP security best practices.
17
+ 3. `mcp-guard audit` mode for client-ready reports.
18
+ 4. Policy file for approved commands, packages, directories, and remote URLs.
19
+ 5. Baseline mode: accept known findings and fail only on new risks.
20
+
21
+ ## Later
22
+
23
+ 1. SBOM/package metadata checks for MCP server packages.
24
+ 2. Local web report viewer.
25
+ 3. Hosted team dashboard only after repeated paid audit demand.
26
+
27
+ ## Product Principles
28
+
29
+ - Local-first by default.
30
+ - Findings must include a fix.
31
+ - Avoid noisy rules that do not change behavior.
32
+ - Prefer workflow integration over dashboards.
33
+ - Services first, SaaS later.
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "agent-mcp-guard",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Open-source CLI scanner for risky MCP server and AI agent tool configuration.",
5
5
  "type": "module",
6
+ "homepage": "https://chaoyue0307.github.io/mcp-guard/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ChaoYue0307/mcp-guard.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/ChaoYue0307/mcp-guard/issues"
13
+ },
6
14
  "bin": {
7
15
  "mcp-guard": "bin/mcp-guard.js"
8
16
  },
@@ -32,8 +40,12 @@
32
40
  "bin",
33
41
  "src",
34
42
  "README.md",
43
+ "action.yml",
35
44
  "LICENSE",
45
+ "SECURITY.md",
36
46
  "docs",
37
- "examples"
47
+ "examples",
48
+ "site/assets/readme-hero.svg",
49
+ "site/assets/brand-mark.svg"
38
50
  ]
39
51
  }
@@ -0,0 +1,6 @@
1
+ <svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="mcp-guard mark">
2
+ <rect width="96" height="96" rx="22" fill="#101312"/>
3
+ <path d="M29 48l19-24 19 24-19 24-19-24z" fill="#2dd4bf"/>
4
+ <path d="M52 23l28 25-28 25" stroke="#f8fafc" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
5
+ </svg>
6
+
@@ -0,0 +1,42 @@
1
+ <svg width="1200" height="420" viewBox="0 0 1200 420" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">mcp-guard scan report</title>
3
+ <desc id="desc">A dark product hero showing an MCP security scan report with critical, high, medium, and low findings.</desc>
4
+ <rect width="1200" height="420" rx="28" fill="#101312"/>
5
+ <path d="M0 0h1200v420H0z" fill="url(#grid)" opacity=".22"/>
6
+ <path d="M925 0h275v150L1015 94 925 132V0z" fill="#0f766e" opacity=".2"/>
7
+ <path d="M0 282l202 64 144-42v116H0V282z" fill="#7c2d12" opacity=".18"/>
8
+ <rect x="70" y="66" width="440" height="288" rx="22" fill="#f8fafc"/>
9
+ <rect x="96" y="94" width="78" height="78" rx="18" fill="#101312"/>
10
+ <path d="M121 133l15-19 15 19-15 19-15-19z" fill="#2dd4bf"/>
11
+ <path d="M139 112l23 21-23 21" stroke="#f8fafc" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <text x="198" y="122" fill="#101312" font-family="Inter, Arial, sans-serif" font-size="34" font-weight="800">mcp-guard</text>
13
+ <text x="198" y="154" fill="#475569" font-family="Inter, Arial, sans-serif" font-size="18">Local-first MCP config security scanner</text>
14
+ <rect x="96" y="196" width="360" height="16" rx="8" fill="#dbeafe"/>
15
+ <rect x="96" y="226" width="270" height="16" rx="8" fill="#ccfbf1"/>
16
+ <rect x="96" y="256" width="322" height="16" rx="8" fill="#ffedd5"/>
17
+ <rect x="96" y="296" width="152" height="40" rx="20" fill="#0f766e"/>
18
+ <text x="125" y="322" fill="#fff" font-family="Inter, Arial, sans-serif" font-size="16" font-weight="700">npm install</text>
19
+ <rect x="546" y="44" width="584" height="332" rx="24" fill="#0b0f0e" stroke="#33413d"/>
20
+ <rect x="546" y="44" width="584" height="54" rx="24" fill="#161d1b"/>
21
+ <circle cx="576" cy="71" r="6" fill="#ef4444"/>
22
+ <circle cx="598" cy="71" r="6" fill="#f59e0b"/>
23
+ <circle cx="620" cy="71" r="6" fill="#10b981"/>
24
+ <text x="652" y="77" fill="#94a3b8" font-family="Menlo, Consolas, monospace" font-size="14">mcp-guard scan --fail-on high</text>
25
+ <text x="586" y="134" fill="#e2e8f0" font-family="Menlo, Consolas, monospace" font-size="20" font-weight="700">scan report</text>
26
+ <text x="586" y="172" fill="#94a3b8" font-family="Menlo, Consolas, monospace" font-size="15">MCP servers: 3 Findings: 9 Risk score: 98</text>
27
+ <rect x="586" y="204" width="138" height="38" rx="12" fill="#7f1d1d"/>
28
+ <text x="610" y="229" fill="#fecaca" font-family="Inter, Arial, sans-serif" font-size="14" font-weight="800">2 critical</text>
29
+ <rect x="742" y="204" width="112" height="38" rx="12" fill="#7c2d12"/>
30
+ <text x="767" y="229" fill="#fed7aa" font-family="Inter, Arial, sans-serif" font-size="14" font-weight="800">5 high</text>
31
+ <rect x="872" y="204" width="124" height="38" rx="12" fill="#713f12"/>
32
+ <text x="897" y="229" fill="#fde68a" font-family="Inter, Arial, sans-serif" font-size="14" font-weight="800">2 medium</text>
33
+ <rect x="586" y="272" width="482" height="1" fill="#33413d"/>
34
+ <text x="586" y="306" fill="#fca5a5" font-family="Menlo, Consolas, monospace" font-size="15">MCP010 shell command executes inline script</text>
35
+ <text x="586" y="334" fill="#fdba74" font-family="Menlo, Consolas, monospace" font-size="15">MCP030 secret-like env var exposed</text>
36
+ <text x="586" y="362" fill="#fef3c7" font-family="Menlo, Consolas, monospace" font-size="15">MCP041 broad filesystem access</text>
37
+ <defs>
38
+ <pattern id="grid" width="38" height="38" patternUnits="userSpaceOnUse">
39
+ <path d="M38 0H0v38" stroke="#f8fafc" stroke-width="1"/>
40
+ </pattern>
41
+ </defs>
42
+ </svg>
package/src/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { scan } from "./scan.js";
4
- import { generateJsonReport, generateMarkdownReport, generateTextReport } from "./report.js";
4
+ import { generateHtmlReport, generateJsonReport, generateMarkdownReport, generateTextReport } from "./report.js";
5
5
  import { compareSeverity, severityRank } from "./severity.js";
6
6
 
7
- const VERSION = "0.1.0";
7
+ const VERSION = "0.2.0";
8
8
 
9
9
  export async function runCli(argv, io) {
10
10
  const args = argv.slice(2);
@@ -80,8 +80,8 @@ function parseScanArgs(args, defaultCwd) {
80
80
  } else if (arg === "--format" || arg === "-f") {
81
81
  options.format = readValue(args, index, arg);
82
82
  index += 1;
83
- if (!["text", "markdown", "json"].includes(options.format)) {
84
- throw new Error("--format must be one of: text, markdown, json");
83
+ if (!["text", "markdown", "json", "html"].includes(options.format)) {
84
+ throw new Error("--format must be one of: text, markdown, json, html");
85
85
  }
86
86
  } else if (arg === "--fail-on") {
87
87
  options.failOn = readValue(args, index, arg);
@@ -121,6 +121,9 @@ function renderReport(result, format) {
121
121
  if (format === "markdown") {
122
122
  return generateMarkdownReport(result);
123
123
  }
124
+ if (format === "html") {
125
+ return generateHtmlReport(result);
126
+ }
124
127
  return generateTextReport(result);
125
128
  }
126
129
 
@@ -142,7 +145,7 @@ Usage:
142
145
  Scan options:
143
146
  -c, --config <path> Scan a specific MCP config file. Can be repeated.
144
147
  -o, --output <path> Write report to a file.
145
- -f, --format <format> text, markdown, or json. Default: text.
148
+ -f, --format <format> text, markdown, json, or html. Default: text.
146
149
  --fail-on <severity> Exit 2 when finding severity is at least threshold.
147
150
  critical, high, medium, low, none. Default: none.
148
151
  --cwd <path> Working directory for project config discovery.
@@ -151,6 +154,7 @@ Scan options:
151
154
  Examples:
152
155
  mcp-guard scan
153
156
  mcp-guard scan --format markdown --output mcp-guard-report.md
157
+ mcp-guard scan --format html --output mcp-guard-report.html
154
158
  mcp-guard scan --config .mcp.json --fail-on high
155
159
  `;
156
160
  }
package/src/report.js CHANGED
@@ -105,6 +105,362 @@ export function generateJsonReport(result) {
105
105
  return JSON.stringify(sanitizeResult(result), null, 2);
106
106
  }
107
107
 
108
+ export function generateHtmlReport(result) {
109
+ const safeResult = sanitizeResult(result);
110
+ const riskTone = riskToneForScore(safeResult.summary.riskScore);
111
+ const findings = safeResult.findings;
112
+ const servers = safeResult.servers;
113
+
114
+ return `<!doctype html>
115
+ <html lang="en">
116
+ <head>
117
+ <meta charset="utf-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1">
119
+ <title>mcp-guard Scan Report</title>
120
+ <style>
121
+ :root {
122
+ color-scheme: light;
123
+ --bg: #f8fafc;
124
+ --panel: #ffffff;
125
+ --ink: #111827;
126
+ --muted: #5b6575;
127
+ --line: #d9e2ec;
128
+ --soft: #eef4f7;
129
+ --critical: #b91c1c;
130
+ --high: #c2410c;
131
+ --medium: #a16207;
132
+ --low: #0f766e;
133
+ --info: #1d4ed8;
134
+ --shadow: 0 20px 55px rgba(15, 23, 42, 0.08);
135
+ }
136
+
137
+ * { box-sizing: border-box; }
138
+
139
+ body {
140
+ margin: 0;
141
+ background: var(--bg);
142
+ color: var(--ink);
143
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
144
+ line-height: 1.5;
145
+ }
146
+
147
+ main {
148
+ width: min(1120px, calc(100% - 32px));
149
+ margin: 0 auto;
150
+ padding: 28px 0 48px;
151
+ }
152
+
153
+ .hero {
154
+ background: #ffffff;
155
+ border: 1px solid var(--line);
156
+ border-radius: 8px;
157
+ box-shadow: var(--shadow);
158
+ padding: 28px;
159
+ display: grid;
160
+ grid-template-columns: minmax(0, 1fr) 240px;
161
+ gap: 24px;
162
+ align-items: stretch;
163
+ }
164
+
165
+ .eyebrow {
166
+ margin: 0 0 8px;
167
+ color: var(--info);
168
+ font-size: 13px;
169
+ font-weight: 700;
170
+ letter-spacing: 0;
171
+ text-transform: uppercase;
172
+ }
173
+
174
+ h1, h2 {
175
+ letter-spacing: 0;
176
+ line-height: 1.08;
177
+ }
178
+
179
+ h1 {
180
+ margin: 0;
181
+ font-size: 34px;
182
+ }
183
+
184
+ h2 {
185
+ margin: 0 0 14px;
186
+ font-size: 21px;
187
+ }
188
+
189
+ .lead {
190
+ max-width: 720px;
191
+ margin: 12px 0 0;
192
+ color: var(--muted);
193
+ font-size: 16px;
194
+ }
195
+
196
+ .scorecard {
197
+ border-radius: 8px;
198
+ border: 1px solid var(--line);
199
+ background: var(--soft);
200
+ padding: 18px;
201
+ min-height: 176px;
202
+ display: flex;
203
+ flex-direction: column;
204
+ justify-content: space-between;
205
+ }
206
+
207
+ .score-label {
208
+ margin: 0;
209
+ color: var(--muted);
210
+ font-size: 13px;
211
+ font-weight: 700;
212
+ text-transform: uppercase;
213
+ }
214
+
215
+ .score-value {
216
+ margin: 8px 0;
217
+ font-size: 58px;
218
+ font-weight: 800;
219
+ line-height: 1;
220
+ color: var(--${riskTone});
221
+ }
222
+
223
+ .score-caption {
224
+ margin: 0;
225
+ color: var(--muted);
226
+ font-size: 14px;
227
+ }
228
+
229
+ .grid {
230
+ display: grid;
231
+ grid-template-columns: repeat(4, minmax(0, 1fr));
232
+ gap: 12px;
233
+ margin: 18px 0 0;
234
+ }
235
+
236
+ .metric {
237
+ background: var(--panel);
238
+ border: 1px solid var(--line);
239
+ border-radius: 8px;
240
+ padding: 14px;
241
+ }
242
+
243
+ .metric strong {
244
+ display: block;
245
+ font-size: 24px;
246
+ line-height: 1.1;
247
+ }
248
+
249
+ .metric span {
250
+ display: block;
251
+ margin-top: 6px;
252
+ color: var(--muted);
253
+ font-size: 13px;
254
+ }
255
+
256
+ section {
257
+ margin-top: 22px;
258
+ background: var(--panel);
259
+ border: 1px solid var(--line);
260
+ border-radius: 8px;
261
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
262
+ padding: 22px;
263
+ }
264
+
265
+ .severity-row {
266
+ display: grid;
267
+ grid-template-columns: repeat(4, minmax(0, 1fr));
268
+ gap: 10px;
269
+ }
270
+
271
+ .severity {
272
+ border: 1px solid var(--line);
273
+ border-radius: 8px;
274
+ padding: 12px;
275
+ min-height: 78px;
276
+ }
277
+
278
+ .severity b {
279
+ display: block;
280
+ font-size: 22px;
281
+ line-height: 1.1;
282
+ }
283
+
284
+ .severity span {
285
+ display: block;
286
+ margin-top: 6px;
287
+ color: var(--muted);
288
+ font-size: 13px;
289
+ text-transform: capitalize;
290
+ }
291
+
292
+ .critical { color: var(--critical); }
293
+ .high { color: var(--high); }
294
+ .medium { color: var(--medium); }
295
+ .low { color: var(--low); }
296
+
297
+ .table-wrap {
298
+ width: 100%;
299
+ overflow-x: auto;
300
+ border: 1px solid var(--line);
301
+ border-radius: 8px;
302
+ }
303
+
304
+ table {
305
+ width: 100%;
306
+ min-width: 760px;
307
+ border-collapse: collapse;
308
+ background: var(--panel);
309
+ }
310
+
311
+ th, td {
312
+ padding: 12px 14px;
313
+ border-bottom: 1px solid var(--line);
314
+ text-align: left;
315
+ vertical-align: top;
316
+ font-size: 14px;
317
+ }
318
+
319
+ th {
320
+ color: #374151;
321
+ background: #f1f5f9;
322
+ font-size: 12px;
323
+ text-transform: uppercase;
324
+ }
325
+
326
+ tr:last-child td { border-bottom: 0; }
327
+
328
+ code {
329
+ padding: 2px 5px;
330
+ border-radius: 5px;
331
+ background: #eef2f7;
332
+ color: #111827;
333
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
334
+ font-size: 0.92em;
335
+ white-space: normal;
336
+ overflow-wrap: anywhere;
337
+ }
338
+
339
+ .pill {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ min-height: 24px;
343
+ padding: 3px 8px;
344
+ border-radius: 999px;
345
+ background: #f1f5f9;
346
+ font-size: 12px;
347
+ font-weight: 700;
348
+ text-transform: uppercase;
349
+ }
350
+
351
+ .pill.critical { background: #fee2e2; }
352
+ .pill.high { background: #ffedd5; }
353
+ .pill.medium { background: #fef3c7; }
354
+ .pill.low { background: #ccfbf1; }
355
+
356
+ .empty {
357
+ margin: 0;
358
+ color: var(--muted);
359
+ border: 1px dashed var(--line);
360
+ border-radius: 8px;
361
+ padding: 16px;
362
+ background: #fbfdff;
363
+ }
364
+
365
+ .notes {
366
+ color: var(--muted);
367
+ font-size: 14px;
368
+ }
369
+
370
+ .notes ul {
371
+ margin: 10px 0 0;
372
+ padding-left: 18px;
373
+ }
374
+
375
+ @media (max-width: 780px) {
376
+ main {
377
+ width: min(100% - 20px, 1120px);
378
+ padding-top: 10px;
379
+ }
380
+
381
+ .hero {
382
+ grid-template-columns: 1fr;
383
+ padding: 20px;
384
+ }
385
+
386
+ h1 { font-size: 28px; }
387
+
388
+ .grid,
389
+ .severity-row {
390
+ grid-template-columns: repeat(2, minmax(0, 1fr));
391
+ }
392
+ }
393
+
394
+ @media (max-width: 460px) {
395
+ .grid,
396
+ .severity-row {
397
+ grid-template-columns: 1fr;
398
+ }
399
+ }
400
+ </style>
401
+ </head>
402
+ <body>
403
+ <main>
404
+ <header class="hero">
405
+ <div>
406
+ <p class="eyebrow">mcp-guard scan report</p>
407
+ <h1>AI agent tool risk review</h1>
408
+ <p class="lead">Local-first review of MCP server configuration, startup commands, remote endpoints, filesystem scope, and secret-like values.</p>
409
+ <div class="grid">
410
+ ${metric("Scanned files", safeResult.summary.scannedFileCount)}
411
+ ${metric("MCP servers", safeResult.summary.serverCount)}
412
+ ${metric("Findings", safeResult.summary.findingCount)}
413
+ ${metric("Generated", formatDate(safeResult.metadata.generatedAt))}
414
+ </div>
415
+ </div>
416
+ <aside class="scorecard" aria-label="Risk score">
417
+ <div>
418
+ <p class="score-label">Risk score</p>
419
+ <p class="score-value">${escapeHtml(safeResult.summary.riskScore)}</p>
420
+ </div>
421
+ <p class="score-caption">${escapeHtml(riskCaption(safeResult.summary.riskScore))}</p>
422
+ </aside>
423
+ </header>
424
+
425
+ <section>
426
+ <h2>Severity Summary</h2>
427
+ <div class="severity-row">
428
+ ${severityCard("critical", safeResult.summary.counts.critical)}
429
+ ${severityCard("high", safeResult.summary.counts.high)}
430
+ ${severityCard("medium", safeResult.summary.counts.medium)}
431
+ ${severityCard("low", safeResult.summary.counts.low)}
432
+ </div>
433
+ </section>
434
+
435
+ <section>
436
+ <h2>Scanned Files</h2>
437
+ ${renderScannedFiles(safeResult)}
438
+ </section>
439
+
440
+ <section>
441
+ <h2>MCP Server Inventory</h2>
442
+ ${renderServerTable(servers, safeResult.metadata.cwd)}
443
+ </section>
444
+
445
+ <section>
446
+ <h2>Findings</h2>
447
+ ${renderFindingsTable(findings)}
448
+ </section>
449
+
450
+ <section class="notes">
451
+ <h2>Review Notes</h2>
452
+ <ul>
453
+ <li>Secret-like values are redacted before rendering this report.</li>
454
+ <li>Review each server before granting access to files, shells, SaaS accounts, or production systems.</li>
455
+ <li>This report assists security review and does not guarantee that every issue was found.</li>
456
+ </ul>
457
+ </section>
458
+ </main>
459
+ </body>
460
+ </html>
461
+ `;
462
+ }
463
+
108
464
  function cell(value) {
109
465
  return String(value).replaceAll("|", "\\|").replaceAll("\n", "<br>");
110
466
  }
@@ -134,3 +490,108 @@ function sanitizeResult(result) {
134
490
  summary: result.summary
135
491
  };
136
492
  }
493
+
494
+ function metric(label, value) {
495
+ return `<div class="metric"><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`;
496
+ }
497
+
498
+ function severityCard(severity, count) {
499
+ return `<div class="severity ${severity}"><b>${escapeHtml(count)}</b><span>${escapeHtml(severity)}</span></div>`;
500
+ }
501
+
502
+ function renderScannedFiles(result) {
503
+ if (result.scannedFiles.length === 0) {
504
+ return `<p class="empty">No MCP config files were found.</p>`;
505
+ }
506
+
507
+ const items = result.scannedFiles
508
+ .map((file) => `<tr><td><code>${escapeHtml(displayPath(file, result.metadata.cwd))}</code></td></tr>`)
509
+ .join("");
510
+
511
+ return `<div class="table-wrap"><table><thead><tr><th>Path</th></tr></thead><tbody>${items}</tbody></table></div>`;
512
+ }
513
+
514
+ function renderServerTable(servers, cwd) {
515
+ if (servers.length === 0) {
516
+ return `<p class="empty">No MCP servers were found.</p>`;
517
+ }
518
+
519
+ const rows = servers.map((server) => {
520
+ const env = kvList(server.env);
521
+ const headers = kvList(server.headers);
522
+ return `<tr>
523
+ <td><strong>${escapeHtml(server.name)}</strong><br><code>${escapeHtml(displayPath(server.configPath, cwd))}</code></td>
524
+ <td>${codeOrDash(server.command)}</td>
525
+ <td>${codeOrDash(server.args.join(" "))}</td>
526
+ <td>${codeOrDash(server.cwd)}</td>
527
+ <td>${codeOrDash(server.url)}</td>
528
+ <td>${env || "-"}</td>
529
+ <td>${headers || "-"}</td>
530
+ </tr>`;
531
+ }).join("");
532
+
533
+ return `<div class="table-wrap"><table>
534
+ <thead><tr><th>Server</th><th>Command</th><th>Args</th><th>CWD</th><th>URL</th><th>Env</th><th>Headers</th></tr></thead>
535
+ <tbody>${rows}</tbody>
536
+ </table></div>`;
537
+ }
538
+
539
+ function renderFindingsTable(findings) {
540
+ if (findings.length === 0) {
541
+ return `<p class="empty">No findings. Keep reviewing new MCP servers and agent tools before adding them.</p>`;
542
+ }
543
+
544
+ const rows = findings.map((finding) => `<tr>
545
+ <td><span class="pill ${escapeHtml(finding.severity)}">${escapeHtml(finding.severity)}</span></td>
546
+ <td><code>${escapeHtml(finding.id)}</code></td>
547
+ <td>${escapeHtml(finding.serverName)}</td>
548
+ <td>${escapeHtml(finding.title)}</td>
549
+ <td>${codeOrDash(finding.evidence)}</td>
550
+ <td>${escapeHtml(finding.recommendation)}</td>
551
+ </tr>`).join("");
552
+
553
+ return `<div class="table-wrap"><table>
554
+ <thead><tr><th>Severity</th><th>Rule</th><th>Server</th><th>Finding</th><th>Evidence</th><th>Recommendation</th></tr></thead>
555
+ <tbody>${rows}</tbody>
556
+ </table></div>`;
557
+ }
558
+
559
+ function kvList(record) {
560
+ return Object.entries(record || {})
561
+ .map(([key, value]) => `<code>${escapeHtml(key)}=${escapeHtml(value)}</code>`)
562
+ .join("<br>");
563
+ }
564
+
565
+ function codeOrDash(value) {
566
+ if (!value) return "-";
567
+ return `<code>${escapeHtml(value)}</code>`;
568
+ }
569
+
570
+ function formatDate(value) {
571
+ const date = new Date(value);
572
+ if (Number.isNaN(date.getTime())) return value;
573
+ return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
574
+ }
575
+
576
+ function riskToneForScore(score) {
577
+ if (score >= 80) return "critical";
578
+ if (score >= 50) return "high";
579
+ if (score >= 20) return "medium";
580
+ return "low";
581
+ }
582
+
583
+ function riskCaption(score) {
584
+ if (score >= 80) return "Critical review recommended before enabling these tools.";
585
+ if (score >= 50) return "High risk configuration; review before team use.";
586
+ if (score >= 20) return "Moderate risk; confirm the intended permission scope.";
587
+ return "Low risk based on the current rule set.";
588
+ }
589
+
590
+ function escapeHtml(value) {
591
+ return String(value ?? "")
592
+ .replaceAll("&", "&amp;")
593
+ .replaceAll("<", "&lt;")
594
+ .replaceAll(">", "&gt;")
595
+ .replaceAll('"', "&quot;")
596
+ .replaceAll("'", "&#39;");
597
+ }