agent-mcp-guard 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/action.yml +5 -5
- package/docs/baseline.md +2 -2
- package/docs/business-playbook.md +3 -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 +9 -13
- package/docs/roadmap.md +1 -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.2"><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.2
|
|
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
|
|
package/action.yml
CHANGED
|
@@ -64,9 +64,9 @@ runs:
|
|
|
64
64
|
using: composite
|
|
65
65
|
steps:
|
|
66
66
|
- name: Set up Node.js
|
|
67
|
-
uses: actions/setup-node@
|
|
67
|
+
uses: actions/setup-node@v6
|
|
68
68
|
with:
|
|
69
|
-
node-version: "
|
|
69
|
+
node-version: "24"
|
|
70
70
|
|
|
71
71
|
- name: Generate reports
|
|
72
72
|
id: reports
|
|
@@ -136,7 +136,7 @@ runs:
|
|
|
136
136
|
|
|
137
137
|
- name: Comment on pull request
|
|
138
138
|
if: ${{ always() && inputs.comment-pr == 'true' && github.event_name == 'pull_request' && steps.reports.outputs.comment-report != '' }}
|
|
139
|
-
uses: actions/github-script@
|
|
139
|
+
uses: actions/github-script@v9
|
|
140
140
|
env:
|
|
141
141
|
MCP_GUARD_COMMENT_PATH: ${{ steps.reports.outputs.comment-report }}
|
|
142
142
|
with:
|
|
@@ -173,14 +173,14 @@ runs:
|
|
|
173
173
|
|
|
174
174
|
- name: Upload report artifact
|
|
175
175
|
if: ${{ always() && inputs.upload-artifact == 'true' }}
|
|
176
|
-
uses: actions/upload-artifact@
|
|
176
|
+
uses: actions/upload-artifact@v7
|
|
177
177
|
with:
|
|
178
178
|
name: ${{ inputs.artifact-name }}
|
|
179
179
|
path: ${{ inputs.output-dir }}
|
|
180
180
|
|
|
181
181
|
- name: Upload SARIF to code scanning
|
|
182
182
|
if: ${{ always() && inputs.upload-sarif == 'true' && steps.reports.outputs.sarif-report != '' }}
|
|
183
|
-
uses: github/codeql-action/upload-sarif@
|
|
183
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
184
184
|
with:
|
|
185
185
|
sarif_file: ${{ steps.reports.outputs.sarif-report }}
|
|
186
186
|
|
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.2
|
|
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.2",
|
|
49
49
|
"findings": [
|
|
50
50
|
{
|
|
51
51
|
"fingerprint": "mcpg_a009b2c2",
|
|
@@ -15,6 +15,7 @@ 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;
|
|
@@ -36,6 +37,8 @@ I built mcp-guard, an open-source local scanner for MCP and AI agent tool config
|
|
|
36
37
|
|
|
37
38
|
It checks for risky shell access, unpinned npx packages, broad filesystem permissions, exposed secrets, and remote MCP servers.
|
|
38
39
|
|
|
40
|
+
It now includes `mcp-guard init`, which creates a GitHub Action workflow and can generate a baseline for accepted current findings.
|
|
41
|
+
|
|
39
42
|
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
43
|
```
|
|
41
44
|
|
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.2
|
|
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.2
|
|
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.2
|
|
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.2
|
|
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.2
|
|
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.2
|
|
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.2
|
|
99
99
|
with:
|
|
100
100
|
config: .mcp.json
|
|
101
101
|
baseline: .mcp-guard-baseline.json
|
package/docs/marketplace.md
CHANGED
|
@@ -79,24 +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.2
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Release notes:
|
|
89
89
|
|
|
90
90
|
```text
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- Supports baseline/allowlist files so known accepted findings do not fail CI.
|
|
97
|
-
- Can post or update a pull request comment with active findings.
|
|
98
|
-
- Can upload SARIF to GitHub code scanning with `upload-sarif: "true"`.
|
|
99
|
-
- Fails workflows by configurable severity threshold.
|
|
91
|
+
CI bootstrap release.
|
|
92
|
+
|
|
93
|
+
- Adds `mcp-guard init` for generating a GitHub Action workflow.
|
|
94
|
+
- Can generate and reference an initial baseline.
|
|
95
|
+
- Keeps Node.js 24, PR comments, artifacts, and SARIF upload support.
|
|
100
96
|
```
|
|
101
97
|
|
|
102
98
|
## Manual Publishing Steps
|
|
@@ -105,11 +101,11 @@ Completed:
|
|
|
105
101
|
|
|
106
102
|
- Public repository created: <https://github.com/ChaoYue0307/mcp-guard-action>
|
|
107
103
|
- `dist/mcp-guard-action/` exported, committed, and pushed.
|
|
108
|
-
-
|
|
104
|
+
- Initial release created: <https://github.com/ChaoYue0307/mcp-guard-action/releases/tag/v0.4.1>
|
|
109
105
|
- README, docs, and website examples now use:
|
|
110
106
|
|
|
111
107
|
```yaml
|
|
112
|
-
- uses: ChaoYue0307/mcp-guard-action@v0.4.
|
|
108
|
+
- uses: ChaoYue0307/mcp-guard-action@v0.4.2
|
|
113
109
|
```
|
|
114
110
|
|
|
115
111
|
Remaining Marketplace web step:
|
package/docs/roadmap.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
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.
|
|
14
15
|
|
|
15
16
|
## Next
|
|
16
17
|
|
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
|
|
300
|
+
<div class="metric"><strong>2026-05-10 13:31 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.2";
|
|
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
|
+
}
|