agent-mcp-guard 0.4.1 → 0.4.3
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 +33 -4
- package/docs/baseline.md +2 -2
- package/docs/business-playbook.md +5 -0
- package/docs/github-action.md +14 -6
- package/docs/launch-checklist.md +1 -1
- package/docs/marketplace-action-readme.md +5 -5
- package/docs/marketplace.md +8 -9
- package/docs/roadmap.md +2 -0
- package/docs/trusted-publishing.md +61 -0
- package/examples/sample-report.md +1 -1
- package/package.json +1 -1
- package/site/e2e/report.html +1 -1
- package/site/e2e/report.json +2 -2
- package/site/e2e/report.md +1 -1
- package/site/e2e/report.sarif +1 -1
- package/src/cli.js +125 -1
- package/src/init.js +255 -0
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Live demo PR: [mcp-guard-demo#1](https://github.com/ChaoYue0307/mcp-guard-demo/p
|
|
|
19
19
|
<a href="https://github.com/marketplace/actions/mcp-guard-mcp-security-scanner"><img alt="GitHub Marketplace" src="https://img.shields.io/badge/Marketplace-mcp--guard-0f766e?logo=github"></a>
|
|
20
20
|
<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>
|
|
21
21
|
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache--2.0-111827"></a>
|
|
22
|
-
<a href="https://github.com/ChaoYue0307/mcp-guard/releases/tag/v0.4.
|
|
22
|
+
<a href="https://github.com/ChaoYue0307/mcp-guard/releases/tag/v0.4.3"><img alt="Release" src="https://img.shields.io/github/v/release/ChaoYue0307/mcp-guard?color=7c2d12"></a>
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
## Install
|
|
@@ -29,6 +29,18 @@ npm install -g agent-mcp-guard
|
|
|
29
29
|
mcp-guard scan
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
Bootstrap a repository with a GitHub Action:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
mcp-guard init
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Bootstrap CI and accept current reviewed findings as a baseline:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mcp-guard init --write-baseline --upload-sarif
|
|
42
|
+
```
|
|
43
|
+
|
|
32
44
|
Scan a specific config:
|
|
33
45
|
|
|
34
46
|
```bash
|
|
@@ -69,7 +81,7 @@ mcp-guard scan --config .mcp.json --baseline .mcp-guard-baseline.json --fail-on
|
|
|
69
81
|
Use the GitHub Action:
|
|
70
82
|
|
|
71
83
|
```yaml
|
|
72
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
84
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
73
85
|
with:
|
|
74
86
|
config: .mcp.json
|
|
75
87
|
baseline: .mcp-guard-baseline.json
|
|
@@ -117,6 +129,14 @@ For the GitHub Action workflow, inspect the public demo repository: [ChaoYue0307
|
|
|
117
129
|
|
|
118
130
|
The GitHub Action can also post an optional pull request comment with the active finding summary.
|
|
119
131
|
|
|
132
|
+
For a guided setup, run:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
mcp-guard init --write-baseline --upload-sarif
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This writes `.github/workflows/mcp-guard.yml` and `.mcp-guard-baseline.json`, using `actions/checkout@v6`, the current Marketplace Action tag, PR comments, and optional SARIF upload.
|
|
139
|
+
|
|
120
140
|
## Example Output
|
|
121
141
|
|
|
122
142
|
```text
|
|
@@ -163,11 +183,19 @@ MCP configs often contain sensitive local paths, internal hostnames, tokens, and
|
|
|
163
183
|
- secret-like values redacted in reports;
|
|
164
184
|
- text, Markdown, HTML, JSON, and SARIF output for local review, CI artifacts, and GitHub code scanning.
|
|
165
185
|
|
|
166
|
-
##
|
|
186
|
+
## Setup Pilot
|
|
167
187
|
|
|
168
188
|
Want to try `mcp-guard` on a real AI agent or MCP setup?
|
|
169
189
|
|
|
170
|
-
The project is
|
|
190
|
+
The project is an automated local scanner. Paid setup help is available for teams that want the CLI, GitHub Action, baseline workflow, PR comments, and SARIF reporting wired into a real repository.
|
|
191
|
+
|
|
192
|
+
Typical scope:
|
|
193
|
+
|
|
194
|
+
- install and run the CLI against redacted local MCP configs;
|
|
195
|
+
- create the GitHub Action workflow;
|
|
196
|
+
- generate and review an initial baseline;
|
|
197
|
+
- enable PR comments and optional GitHub code scanning;
|
|
198
|
+
- record missing rules or config shapes as product feedback.
|
|
171
199
|
|
|
172
200
|
Contact: [hechaoyue0307@gmail.com](mailto:hechaoyue0307@gmail.com)
|
|
173
201
|
|
|
@@ -178,6 +206,7 @@ Contact: [hechaoyue0307@gmail.com](mailto:hechaoyue0307@gmail.com)
|
|
|
178
206
|
- [GitHub Action](docs/github-action.md)
|
|
179
207
|
- [Marketplace publishing plan](docs/marketplace.md)
|
|
180
208
|
- [Privacy and security](docs/privacy-and-security.md)
|
|
209
|
+
- [Trusted publishing](docs/trusted-publishing.md)
|
|
181
210
|
- [Roadmap](docs/roadmap.md)
|
|
182
211
|
- [Operator runbook](docs/operator-runbook.md)
|
|
183
212
|
|
package/docs/baseline.md
CHANGED
|
@@ -30,7 +30,7 @@ If the scan finds only baseline-accepted findings, the exit code is `0`. If a ne
|
|
|
30
30
|
## GitHub Action
|
|
31
31
|
|
|
32
32
|
```yaml
|
|
33
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
33
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
34
34
|
with:
|
|
35
35
|
config: .mcp.json
|
|
36
36
|
baseline: .mcp-guard-baseline.json
|
|
@@ -45,7 +45,7 @@ The generated Markdown, HTML, JSON, and PR comment separate active findings from
|
|
|
45
45
|
{
|
|
46
46
|
"version": 1,
|
|
47
47
|
"generatedAt": "2026-05-10T00:00:00.000Z",
|
|
48
|
-
"toolVersion": "0.4.
|
|
48
|
+
"toolVersion": "0.4.3",
|
|
49
49
|
"findings": [
|
|
50
50
|
{
|
|
51
51
|
"fingerprint": "mcpg_a009b2c2",
|
|
@@ -15,12 +15,15 @@ This is setup and product onboarding, not a manual security audit.
|
|
|
15
15
|
Deliverables:
|
|
16
16
|
|
|
17
17
|
- install the CLI and GitHub Action;
|
|
18
|
+
- run `mcp-guard init` or generate an equivalent workflow manually;
|
|
18
19
|
- generate Markdown, HTML, JSON, and SARIF reports;
|
|
19
20
|
- create an initial baseline for accepted known findings;
|
|
20
21
|
- enable PR comments and optional SARIF upload;
|
|
21
22
|
- document missing rule requests for future product work;
|
|
22
23
|
- provide a short setup handoff note.
|
|
23
24
|
|
|
25
|
+
For product operations, npm Trusted Publishing should be used after the package setting is configured. This avoids manual QR-code publish flows and makes small releases repeatable.
|
|
26
|
+
|
|
24
27
|
## Pricing
|
|
25
28
|
|
|
26
29
|
| Customer | Price |
|
|
@@ -36,6 +39,8 @@ I built mcp-guard, an open-source local scanner for MCP and AI agent tool config
|
|
|
36
39
|
|
|
37
40
|
It checks for risky shell access, unpinned npx packages, broad filesystem permissions, exposed secrets, and remote MCP servers.
|
|
38
41
|
|
|
42
|
+
It now includes `mcp-guard init`, which creates a GitHub Action workflow and can generate a baseline for accepted current findings.
|
|
43
|
+
|
|
39
44
|
I am collecting real-world MCP and AI agent config patterns from teams using Claude, Cursor, Codex, or MCP in production-like workflows. If you can share a redacted config or run the CLI locally, your feedback can help improve the scanner's rules and reports.
|
|
40
45
|
```
|
|
41
46
|
|
package/docs/github-action.md
CHANGED
|
@@ -10,6 +10,14 @@ Marketplace/action repository: <https://github.com/ChaoYue0307/mcp-guard-action>
|
|
|
10
10
|
|
|
11
11
|
## Basic Workflow
|
|
12
12
|
|
|
13
|
+
Fastest setup:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mcp-guard init
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That creates `.github/workflows/mcp-guard.yml` with PR comments enabled. Use `mcp-guard init --write-baseline --upload-sarif` when you want to accept current reviewed findings and send SARIF to GitHub code scanning.
|
|
20
|
+
|
|
13
21
|
```yaml
|
|
14
22
|
name: mcp-guard
|
|
15
23
|
|
|
@@ -26,8 +34,8 @@ jobs:
|
|
|
26
34
|
scan:
|
|
27
35
|
runs-on: ubuntu-latest
|
|
28
36
|
steps:
|
|
29
|
-
- uses: actions/checkout@
|
|
30
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
31
39
|
with:
|
|
32
40
|
config: .mcp.json
|
|
33
41
|
fail-on: high
|
|
@@ -54,8 +62,8 @@ jobs:
|
|
|
54
62
|
scan:
|
|
55
63
|
runs-on: ubuntu-latest
|
|
56
64
|
steps:
|
|
57
|
-
- uses: actions/checkout@
|
|
58
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
65
|
+
- uses: actions/checkout@v6
|
|
66
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
59
67
|
with:
|
|
60
68
|
config: .mcp.json
|
|
61
69
|
fail-on: high
|
|
@@ -67,7 +75,7 @@ jobs:
|
|
|
67
75
|
Use `fail-on: none` when you want artifacts and summaries without blocking a pull request.
|
|
68
76
|
|
|
69
77
|
```yaml
|
|
70
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
78
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
71
79
|
with:
|
|
72
80
|
fail-on: none
|
|
73
81
|
```
|
|
@@ -83,7 +91,7 @@ mcp-guard scan --config .mcp.json --write-baseline .mcp-guard-baseline.json
|
|
|
83
91
|
Commit `.mcp-guard-baseline.json`, then reference it from the action:
|
|
84
92
|
|
|
85
93
|
```yaml
|
|
86
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
94
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
87
95
|
with:
|
|
88
96
|
config: .mcp.json
|
|
89
97
|
baseline: .mcp-guard-baseline.json
|
package/docs/launch-checklist.md
CHANGED
|
@@ -22,8 +22,8 @@ jobs:
|
|
|
22
22
|
scan:
|
|
23
23
|
runs-on: ubuntu-latest
|
|
24
24
|
steps:
|
|
25
|
-
- uses: actions/checkout@
|
|
26
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
25
|
+
- uses: actions/checkout@v6
|
|
26
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
27
27
|
with:
|
|
28
28
|
config: .mcp.json
|
|
29
29
|
fail-on: high
|
|
@@ -41,8 +41,8 @@ jobs:
|
|
|
41
41
|
scan:
|
|
42
42
|
runs-on: ubuntu-latest
|
|
43
43
|
steps:
|
|
44
|
-
- uses: actions/checkout@
|
|
45
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
44
|
+
- uses: actions/checkout@v6
|
|
45
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
46
46
|
with:
|
|
47
47
|
config: .mcp.json
|
|
48
48
|
fail-on: high
|
|
@@ -95,7 +95,7 @@ mcp-guard scan --config .mcp.json --write-baseline .mcp-guard-baseline.json
|
|
|
95
95
|
Then enforce only new findings:
|
|
96
96
|
|
|
97
97
|
```yaml
|
|
98
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
98
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
99
99
|
with:
|
|
100
100
|
config: .mcp.json
|
|
101
101
|
baseline: .mcp-guard-baseline.json
|
package/docs/marketplace.md
CHANGED
|
@@ -79,21 +79,20 @@ Secondary category:
|
|
|
79
79
|
Code quality
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Current release title:
|
|
83
83
|
|
|
84
84
|
```text
|
|
85
|
-
v0.4.
|
|
85
|
+
v0.4.3
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Release notes:
|
|
89
89
|
|
|
90
90
|
```text
|
|
91
|
-
|
|
91
|
+
Trusted Publishing readiness release.
|
|
92
92
|
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- Uses CodeQL SARIF upload v4.
|
|
93
|
+
- Adds a GitHub Actions workflow for npm Trusted Publishing readiness.
|
|
94
|
+
- Keeps `mcp-guard init` for generating a GitHub Action workflow and baseline.
|
|
95
|
+
- Keeps Node.js 24, PR comments, artifacts, and SARIF upload support.
|
|
97
96
|
```
|
|
98
97
|
|
|
99
98
|
## Manual Publishing Steps
|
|
@@ -102,11 +101,11 @@ Completed:
|
|
|
102
101
|
|
|
103
102
|
- Public repository created: <https://github.com/ChaoYue0307/mcp-guard-action>
|
|
104
103
|
- `dist/mcp-guard-action/` exported, committed, and pushed.
|
|
105
|
-
-
|
|
104
|
+
- Initial release created: <https://github.com/ChaoYue0307/mcp-guard-action/releases/tag/v0.4.1>
|
|
106
105
|
- README, docs, and website examples now use:
|
|
107
106
|
|
|
108
107
|
```yaml
|
|
109
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
108
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.3
|
|
110
109
|
```
|
|
111
110
|
|
|
112
111
|
Remaining Marketplace web step:
|
package/docs/roadmap.md
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
- GitHub Action wrapper that writes a job summary, uploads Markdown/HTML/JSON/SARIF artifacts, and can upload SARIF to GitHub code scanning.
|
|
12
12
|
- Baseline/allowlist mode for accepting known findings and failing only on new risks.
|
|
13
13
|
- Optional GitHub pull request comments from the Marketplace Action.
|
|
14
|
+
- `mcp-guard init` for bootstrapping a GitHub Action workflow and optional baseline.
|
|
15
|
+
- npm Trusted Publishing workflow prepared for tokenless release publishing.
|
|
14
16
|
|
|
15
17
|
## Next
|
|
16
18
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Trusted Publishing
|
|
2
|
+
|
|
3
|
+
`mcp-guard` includes a GitHub Actions workflow for npm Trusted Publishing so future releases can publish without npm browser QR links, OTP prompts, or long-lived npm tokens.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
npm browser authentication links can expire quickly and may open as 404. Trusted Publishing lets npm accept a publish from a specific GitHub Actions workflow through OIDC.
|
|
8
|
+
|
|
9
|
+
The workflow is committed at:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
.github/workflows/publish-npm.yml
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It uses:
|
|
16
|
+
|
|
17
|
+
- `permissions: id-token: write`
|
|
18
|
+
- `actions/checkout@v6`
|
|
19
|
+
- `actions/setup-node@v6`
|
|
20
|
+
- Node.js 24
|
|
21
|
+
- `npm publish --access public`
|
|
22
|
+
|
|
23
|
+
## npm Package Settings
|
|
24
|
+
|
|
25
|
+
Configure this once on npmjs.com:
|
|
26
|
+
|
|
27
|
+
| Field | Value |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| Package | `agent-mcp-guard` |
|
|
30
|
+
| Publisher | GitHub Actions |
|
|
31
|
+
| Organization or user | `ChaoYue0307` |
|
|
32
|
+
| Repository | `mcp-guard` |
|
|
33
|
+
| Workflow filename | `publish-npm.yml` |
|
|
34
|
+
| Environment name | leave empty |
|
|
35
|
+
|
|
36
|
+
After this is saved, run the workflow from GitHub Actions with the release tag, for example:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
v0.4.3
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Release Flow After Setup
|
|
43
|
+
|
|
44
|
+
1. Update `package.json` and `src/cli.js`.
|
|
45
|
+
2. Run `npm test` and `npm run release:check`.
|
|
46
|
+
3. Commit and push to `main`.
|
|
47
|
+
4. Create a GitHub release tag such as `v0.4.3`.
|
|
48
|
+
5. Run the `Publish npm` workflow with the same tag.
|
|
49
|
+
6. Verify npm:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm view agent-mcp-guard version
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Troubleshooting
|
|
56
|
+
|
|
57
|
+
- The workflow filename configured on npm must be exactly `publish-npm.yml`.
|
|
58
|
+
- The package `repository.url` must match `https://github.com/ChaoYue0307/mcp-guard`.
|
|
59
|
+
- The workflow must run on GitHub-hosted runners.
|
|
60
|
+
- The workflow must keep `id-token: write`.
|
|
61
|
+
- If npm says authentication failed, re-check the npm Trusted Publisher fields before retrying.
|
package/package.json
CHANGED
package/site/e2e/report.html
CHANGED
|
@@ -297,7 +297,7 @@
|
|
|
297
297
|
<div class="metric"><strong>1</strong><span>Scanned files</span></div>
|
|
298
298
|
<div class="metric"><strong>3</strong><span>MCP servers</span></div>
|
|
299
299
|
<div class="metric"><strong>9</strong><span>Active findings</span></div>
|
|
300
|
-
<div class="metric"><strong>2026-05-10 13:
|
|
300
|
+
<div class="metric"><strong>2026-05-10 13:47 UTC</strong><span>Generated</span></div>
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
<aside class="scorecard" aria-label="Risk score">
|
package/site/e2e/report.json
CHANGED
package/site/e2e/report.md
CHANGED
package/site/e2e/report.sarif
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { applyBaseline, loadBaselineFile, writeBaselineFile } from "./baseline.js";
|
|
4
|
+
import { initProject, renderInitSummary } from "./init.js";
|
|
4
5
|
import { scan } from "./scan.js";
|
|
5
6
|
import { generateHtmlReport, generateJsonReport, generateMarkdownReport, generateSarifReport, generateTextReport } from "./report.js";
|
|
6
7
|
import { compareSeverity, severityRank } from "./severity.js";
|
|
7
8
|
|
|
8
|
-
const VERSION = "0.4.
|
|
9
|
+
const VERSION = "0.4.3";
|
|
9
10
|
|
|
10
11
|
export async function runCli(argv, io) {
|
|
11
12
|
const args = argv.slice(2);
|
|
@@ -21,6 +22,34 @@ export async function runCli(argv, io) {
|
|
|
21
22
|
return 0;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
if (command === "init") {
|
|
26
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
27
|
+
io.stdout.write(helpText());
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const options = parseInitArgs(args.slice(1), io.cwd);
|
|
32
|
+
const result = await initProject({
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
env: io.env,
|
|
35
|
+
configPaths: options.configPaths,
|
|
36
|
+
includeDefaults: options.includeDefaults,
|
|
37
|
+
workflowPath: options.workflowPath,
|
|
38
|
+
baselinePath: options.baselinePath,
|
|
39
|
+
failOn: options.failOn,
|
|
40
|
+
commentPr: options.commentPr,
|
|
41
|
+
uploadSarif: options.uploadSarif,
|
|
42
|
+
writeBaseline: options.writeBaseline,
|
|
43
|
+
useBaseline: options.useBaseline,
|
|
44
|
+
baselineReason: options.baselineReason,
|
|
45
|
+
force: options.force,
|
|
46
|
+
dryRun: options.dryRun,
|
|
47
|
+
toolVersion: VERSION
|
|
48
|
+
});
|
|
49
|
+
io.stdout.write(renderInitSummary(result, options.cwd));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
24
53
|
if (command !== "scan") {
|
|
25
54
|
io.stderr.write(`Unknown command: ${command}\n\n`);
|
|
26
55
|
io.stderr.write(helpText());
|
|
@@ -70,6 +99,82 @@ export async function runCli(argv, io) {
|
|
|
70
99
|
return 0;
|
|
71
100
|
}
|
|
72
101
|
|
|
102
|
+
function parseInitArgs(args, defaultCwd) {
|
|
103
|
+
const options = {
|
|
104
|
+
cwd: defaultCwd,
|
|
105
|
+
configPaths: [],
|
|
106
|
+
includeDefaults: true,
|
|
107
|
+
workflowPath: "",
|
|
108
|
+
baselinePath: "",
|
|
109
|
+
failOn: "high",
|
|
110
|
+
commentPr: true,
|
|
111
|
+
uploadSarif: false,
|
|
112
|
+
writeBaseline: false,
|
|
113
|
+
useBaseline: false,
|
|
114
|
+
baselineReason: "Accepted current MCP findings",
|
|
115
|
+
force: false,
|
|
116
|
+
dryRun: false
|
|
117
|
+
};
|
|
118
|
+
options.workflowPath = path.join(options.cwd, ".github", "workflows", "mcp-guard.yml");
|
|
119
|
+
options.baselinePath = path.join(options.cwd, ".mcp-guard-baseline.json");
|
|
120
|
+
let workflowPathProvided = false;
|
|
121
|
+
let baselinePathProvided = false;
|
|
122
|
+
|
|
123
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
124
|
+
const arg = args[index];
|
|
125
|
+
if (arg === "--config" || arg === "-c") {
|
|
126
|
+
options.configPaths.push(resolveInputPath(readValue(args, index, arg), options.cwd));
|
|
127
|
+
index += 1;
|
|
128
|
+
} else if (arg === "--workflow") {
|
|
129
|
+
options.workflowPath = resolveInputPath(readValue(args, index, arg), options.cwd);
|
|
130
|
+
workflowPathProvided = true;
|
|
131
|
+
index += 1;
|
|
132
|
+
} else if (arg === "--baseline" || arg === "--allowlist") {
|
|
133
|
+
options.baselinePath = resolveInputPath(readValue(args, index, arg), options.cwd);
|
|
134
|
+
options.useBaseline = true;
|
|
135
|
+
baselinePathProvided = true;
|
|
136
|
+
index += 1;
|
|
137
|
+
} else if (arg === "--write-baseline" || arg === "--write-allowlist") {
|
|
138
|
+
options.writeBaseline = true;
|
|
139
|
+
options.useBaseline = true;
|
|
140
|
+
} else if (arg === "--baseline-reason") {
|
|
141
|
+
options.baselineReason = readValue(args, index, arg);
|
|
142
|
+
index += 1;
|
|
143
|
+
} else if (arg === "--fail-on") {
|
|
144
|
+
options.failOn = readValue(args, index, arg);
|
|
145
|
+
index += 1;
|
|
146
|
+
if (!["critical", "high", "medium", "low", "none"].includes(options.failOn)) {
|
|
147
|
+
throw new Error("--fail-on must be one of: critical, high, medium, low, none");
|
|
148
|
+
}
|
|
149
|
+
} else if (arg === "--comment-pr") {
|
|
150
|
+
options.commentPr = true;
|
|
151
|
+
} else if (arg === "--no-comment-pr") {
|
|
152
|
+
options.commentPr = false;
|
|
153
|
+
} else if (arg === "--upload-sarif") {
|
|
154
|
+
options.uploadSarif = true;
|
|
155
|
+
} else if (arg === "--cwd") {
|
|
156
|
+
options.cwd = path.resolve(readValue(args, index, arg));
|
|
157
|
+
if (!workflowPathProvided) {
|
|
158
|
+
options.workflowPath = path.join(options.cwd, ".github", "workflows", "mcp-guard.yml");
|
|
159
|
+
}
|
|
160
|
+
if (!baselinePathProvided) {
|
|
161
|
+
options.baselinePath = path.join(options.cwd, ".mcp-guard-baseline.json");
|
|
162
|
+
}
|
|
163
|
+
index += 1;
|
|
164
|
+
} else if (arg === "--no-defaults") {
|
|
165
|
+
options.includeDefaults = false;
|
|
166
|
+
} else if (arg === "--force") {
|
|
167
|
+
options.force = true;
|
|
168
|
+
} else if (arg === "--dry-run") {
|
|
169
|
+
options.dryRun = true;
|
|
170
|
+
} else {
|
|
171
|
+
throw new Error(`Unknown init option: ${arg}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return options;
|
|
176
|
+
}
|
|
177
|
+
|
|
73
178
|
function parseScanArgs(args, defaultCwd) {
|
|
74
179
|
const options = {
|
|
75
180
|
cwd: defaultCwd,
|
|
@@ -165,9 +270,26 @@ Open-source scanner for risky MCP server and AI agent tool configuration.
|
|
|
165
270
|
|
|
166
271
|
Usage:
|
|
167
272
|
mcp-guard scan [options]
|
|
273
|
+
mcp-guard init [options]
|
|
168
274
|
mcp-guard version
|
|
169
275
|
mcp-guard help
|
|
170
276
|
|
|
277
|
+
Init options:
|
|
278
|
+
--workflow <path> Workflow path to create. Default: .github/workflows/mcp-guard.yml.
|
|
279
|
+
-c, --config <path> MCP config path to reference in the workflow. Can be repeated for baseline generation.
|
|
280
|
+
--fail-on <severity> Workflow fail threshold. Default: high.
|
|
281
|
+
--baseline <path> Reference an existing baseline JSON file in the workflow.
|
|
282
|
+
--write-baseline Generate a baseline from current findings and reference it in the workflow.
|
|
283
|
+
--baseline-reason <text>
|
|
284
|
+
Reason stored for newly written baseline entries.
|
|
285
|
+
--comment-pr Enable pull request comments. Default.
|
|
286
|
+
--no-comment-pr Do not add pull request comment permission or input.
|
|
287
|
+
--upload-sarif Upload SARIF to GitHub code scanning.
|
|
288
|
+
--cwd <path> Project directory to initialize.
|
|
289
|
+
--no-defaults Only scan paths passed with --config for baseline generation.
|
|
290
|
+
--force Overwrite existing workflow or baseline files.
|
|
291
|
+
--dry-run Print planned files without writing them.
|
|
292
|
+
|
|
171
293
|
Scan options:
|
|
172
294
|
-c, --config <path> Scan a specific MCP config file. Can be repeated.
|
|
173
295
|
-o, --output <path> Write report to a file.
|
|
@@ -183,6 +305,8 @@ Scan options:
|
|
|
183
305
|
--no-defaults Only scan paths passed with --config.
|
|
184
306
|
|
|
185
307
|
Examples:
|
|
308
|
+
mcp-guard init
|
|
309
|
+
mcp-guard init --write-baseline --upload-sarif
|
|
186
310
|
mcp-guard scan
|
|
187
311
|
mcp-guard scan --format markdown --output mcp-guard-report.md
|
|
188
312
|
mcp-guard scan --format html --output mcp-guard-report.html
|
package/src/init.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeBaselineFile } from "./baseline.js";
|
|
4
|
+
import { discoverConfigFiles } from "./discovery.js";
|
|
5
|
+
import { displayPath } from "./fingerprint.js";
|
|
6
|
+
import { scan } from "./scan.js";
|
|
7
|
+
|
|
8
|
+
export async function initProject({
|
|
9
|
+
cwd,
|
|
10
|
+
env,
|
|
11
|
+
configPaths = [],
|
|
12
|
+
includeDefaults = true,
|
|
13
|
+
workflowPath,
|
|
14
|
+
baselinePath,
|
|
15
|
+
failOn = "high",
|
|
16
|
+
commentPr = true,
|
|
17
|
+
uploadSarif = false,
|
|
18
|
+
writeBaseline = false,
|
|
19
|
+
useBaseline = false,
|
|
20
|
+
baselineReason = "Accepted current MCP findings",
|
|
21
|
+
force = false,
|
|
22
|
+
dryRun = false,
|
|
23
|
+
toolVersion
|
|
24
|
+
}) {
|
|
25
|
+
const discoveredConfigPaths = configPaths.length === 0 && includeDefaults
|
|
26
|
+
? await discoverConfigFiles({ cwd, env })
|
|
27
|
+
: [];
|
|
28
|
+
const workflowConfigPath = selectWorkflowConfigPath({
|
|
29
|
+
cwd,
|
|
30
|
+
explicitConfigPaths: configPaths,
|
|
31
|
+
discoveredConfigPaths
|
|
32
|
+
});
|
|
33
|
+
const files = [];
|
|
34
|
+
|
|
35
|
+
if (writeBaseline) {
|
|
36
|
+
const baselineConfigPaths = configPaths.length > 0
|
|
37
|
+
? configPaths
|
|
38
|
+
: workflowConfigPath
|
|
39
|
+
? [workflowConfigPath]
|
|
40
|
+
: [];
|
|
41
|
+
if (baselineConfigPaths.length === 0) {
|
|
42
|
+
throw new Error("No project MCP config found for baseline generation. Add .mcp.json, pass --config, or run init without --write-baseline.");
|
|
43
|
+
}
|
|
44
|
+
const result = await scan({
|
|
45
|
+
cwd,
|
|
46
|
+
env,
|
|
47
|
+
configPaths: baselineConfigPaths,
|
|
48
|
+
includeDefaults: false,
|
|
49
|
+
toolVersion
|
|
50
|
+
});
|
|
51
|
+
const baseline = await writeBaselineFileIfAllowed(baselinePath, result, {
|
|
52
|
+
reason: baselineReason,
|
|
53
|
+
force,
|
|
54
|
+
dryRun
|
|
55
|
+
});
|
|
56
|
+
files.push({
|
|
57
|
+
type: "baseline",
|
|
58
|
+
path: baselinePath,
|
|
59
|
+
action: baseline.action,
|
|
60
|
+
findingCount: baseline.findingCount
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const workflow = renderGithubWorkflow({
|
|
65
|
+
actionRef: `ChaoYue0307/mcp-guard-action@v${toolVersion}`,
|
|
66
|
+
configPath: workflowConfigPath ? displayPath(workflowConfigPath, cwd) : "",
|
|
67
|
+
baselinePath: useBaseline || writeBaseline ? displayPath(baselinePath, cwd) : "",
|
|
68
|
+
failOn,
|
|
69
|
+
commentPr,
|
|
70
|
+
uploadSarif
|
|
71
|
+
});
|
|
72
|
+
const workflowWrite = await writeTextFileIfAllowed(workflowPath, workflow, { force, dryRun });
|
|
73
|
+
files.push({
|
|
74
|
+
type: "workflow",
|
|
75
|
+
path: workflowPath,
|
|
76
|
+
action: workflowWrite.action
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
dryRun,
|
|
81
|
+
workflowPath,
|
|
82
|
+
baselinePath,
|
|
83
|
+
configPath: workflowConfigPath,
|
|
84
|
+
discoveredConfigPaths,
|
|
85
|
+
files,
|
|
86
|
+
nextSteps: buildNextSteps({ workflowPath, baselinePath, writeBaseline, uploadSarif })
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function renderInitSummary(result, cwd) {
|
|
91
|
+
const lines = ["mcp-guard init completed"];
|
|
92
|
+
|
|
93
|
+
if (result.dryRun) {
|
|
94
|
+
lines[0] = "mcp-guard init dry run";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (result.configPath) {
|
|
98
|
+
lines.push(`Config: ${displayPath(result.configPath, cwd)}`);
|
|
99
|
+
} else {
|
|
100
|
+
lines.push("Config: default discovery paths");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const file of result.files) {
|
|
104
|
+
const label = file.action === "skipped" ? "Skipped" : actionLabel(file.action);
|
|
105
|
+
const suffix = file.type === "baseline" ? ` (${file.findingCount} findings)` : "";
|
|
106
|
+
lines.push(`${label}: ${displayPath(file.path, cwd)}${suffix}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push("Next:");
|
|
111
|
+
for (const step of result.nextSteps) {
|
|
112
|
+
lines.push(`- ${step}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `${lines.join("\n")}\n`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function renderGithubWorkflow({ actionRef, configPath, baselinePath, failOn, commentPr, uploadSarif }) {
|
|
119
|
+
const permissions = [" contents: read"];
|
|
120
|
+
if (commentPr) {
|
|
121
|
+
permissions.push(" pull-requests: write");
|
|
122
|
+
}
|
|
123
|
+
if (uploadSarif) {
|
|
124
|
+
permissions.push(" security-events: write");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const inputs = [];
|
|
128
|
+
if (configPath) {
|
|
129
|
+
inputs.push(` config: ${quoteYaml(configPath)}`);
|
|
130
|
+
}
|
|
131
|
+
if (baselinePath) {
|
|
132
|
+
inputs.push(` baseline: ${quoteYaml(baselinePath)}`);
|
|
133
|
+
}
|
|
134
|
+
inputs.push(` fail-on: ${quoteYaml(failOn)}`);
|
|
135
|
+
if (commentPr) {
|
|
136
|
+
inputs.push(' comment-pr: "true"');
|
|
137
|
+
}
|
|
138
|
+
if (uploadSarif) {
|
|
139
|
+
inputs.push(' upload-sarif: "true"');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return `name: mcp-guard
|
|
143
|
+
|
|
144
|
+
on:
|
|
145
|
+
pull_request:
|
|
146
|
+
push:
|
|
147
|
+
branches: [main]
|
|
148
|
+
workflow_dispatch:
|
|
149
|
+
|
|
150
|
+
permissions:
|
|
151
|
+
${permissions.join("\n")}
|
|
152
|
+
|
|
153
|
+
jobs:
|
|
154
|
+
scan:
|
|
155
|
+
runs-on: ubuntu-latest
|
|
156
|
+
steps:
|
|
157
|
+
- uses: actions/checkout@v6
|
|
158
|
+
- uses: ${actionRef}
|
|
159
|
+
with:
|
|
160
|
+
${inputs.join("\n")}
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function selectWorkflowConfigPath({ cwd, explicitConfigPaths, discoveredConfigPaths }) {
|
|
165
|
+
if (explicitConfigPaths.length > 0) {
|
|
166
|
+
return explicitConfigPaths[0];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return discoveredConfigPaths.find((filePath) => isInsideDirectory(filePath, cwd)) || "";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function writeBaselineFileIfAllowed(filePath, result, { reason, force, dryRun }) {
|
|
173
|
+
const existing = await fileExists(filePath);
|
|
174
|
+
if (existing && !force) {
|
|
175
|
+
throw new Error(`Refusing to overwrite ${filePath}; use --force to replace it.`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (dryRun) {
|
|
179
|
+
return {
|
|
180
|
+
action: existing ? "would-overwrite" : "would-create",
|
|
181
|
+
findingCount: result.findings.length + result.acceptedFindings.length
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const baseline = await writeBaselineFile(filePath, result, { reason });
|
|
186
|
+
return {
|
|
187
|
+
action: existing ? "overwritten" : "created",
|
|
188
|
+
findingCount: baseline.findings.length
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function writeTextFileIfAllowed(filePath, content, { force, dryRun }) {
|
|
193
|
+
const existing = await fileExists(filePath);
|
|
194
|
+
if (existing && !force) {
|
|
195
|
+
throw new Error(`Refusing to overwrite ${filePath}; use --force to replace it.`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (dryRun) {
|
|
199
|
+
return {
|
|
200
|
+
action: existing ? "would-overwrite" : "would-create"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
205
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
206
|
+
return {
|
|
207
|
+
action: existing ? "overwritten" : "created"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function fileExists(filePath) {
|
|
212
|
+
try {
|
|
213
|
+
await fs.access(filePath);
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildNextSteps({ workflowPath, baselinePath, writeBaseline, uploadSarif }) {
|
|
221
|
+
const steps = [
|
|
222
|
+
`Review ${path.basename(workflowPath)} before committing it.`,
|
|
223
|
+
"Run mcp-guard scan locally and confirm the findings are expected.",
|
|
224
|
+
"Commit the workflow and open a pull request that changes MCP config to verify the check."
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
if (writeBaseline) {
|
|
228
|
+
steps.splice(1, 0, `Review ${path.basename(baselinePath)} because accepted findings will not fail CI.`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (uploadSarif) {
|
|
232
|
+
steps.push("Confirm GitHub code scanning is enabled for SARIF results.");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return steps;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function actionLabel(action) {
|
|
239
|
+
if (action === "would-create") return "Would create";
|
|
240
|
+
if (action === "would-overwrite") return "Would overwrite";
|
|
241
|
+
if (action === "overwritten") return "Overwrote";
|
|
242
|
+
return "Created";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isInsideDirectory(filePath, directory) {
|
|
246
|
+
const relative = path.relative(path.resolve(directory), path.resolve(filePath));
|
|
247
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function quoteYaml(value) {
|
|
251
|
+
if (/^[A-Za-z0-9_./-]+$/.test(value)) {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
return JSON.stringify(value);
|
|
255
|
+
}
|