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 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.0"><img alt="Release" src="https://img.shields.io/github/v/release/ChaoYue0307/mcp-guard?color=7c2d12"></a>
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.0
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
- ## Early Access and Feedback
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 currently an automated local scanner. I am collecting early users, real-world config examples, CI setup feedback, baseline workflow feedback, and rule requests to improve coverage.
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@v4
67
+ uses: actions/setup-node@v6
68
68
  with:
69
- node-version: "20"
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@v7
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@v4
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@v3
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.0
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.0",
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
 
@@ -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@v4
30
- - uses: ChaoYue0307/mcp-guard-action@v0.4.0
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@v4
58
- - uses: ChaoYue0307/mcp-guard-action@v0.4.0
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.0
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.0
94
+ - uses: ChaoYue0307/mcp-guard-action@v0.4.2
87
95
  with:
88
96
  config: .mcp.json
89
97
  baseline: .mcp-guard-baseline.json
@@ -31,7 +31,7 @@ mcp-guard scan --config .mcp.json --fail-on high
31
31
  ## GitHub Action Setup
32
32
 
33
33
  ```yaml
34
- - uses: ChaoYue0307/mcp-guard-action@v0.4.0
34
+ - uses: ChaoYue0307/mcp-guard-action@v0.4.2
35
35
  with:
36
36
  config: .mcp.json
37
37
  baseline: .mcp-guard-baseline.json
@@ -22,8 +22,8 @@ jobs:
22
22
  scan:
23
23
  runs-on: ubuntu-latest
24
24
  steps:
25
- - uses: actions/checkout@v4
26
- - uses: ChaoYue0307/mcp-guard-action@v0.4.0
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@v4
45
- - uses: ChaoYue0307/mcp-guard-action@v0.4.0
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.0
98
+ - uses: ChaoYue0307/mcp-guard-action@v0.4.2
99
99
  with:
100
100
  config: .mcp.json
101
101
  baseline: .mcp-guard-baseline.json
@@ -79,24 +79,20 @@ Secondary category:
79
79
  Code quality
80
80
  ```
81
81
 
82
- Release title:
82
+ Current release title:
83
83
 
84
84
  ```text
85
- v0.4.0
85
+ v0.4.2
86
86
  ```
87
87
 
88
88
  Release notes:
89
89
 
90
90
  ```text
91
- Baseline and pull request comment release.
92
-
93
- - Runs mcp-guard from the pinned action tag.
94
- - Generates Markdown, HTML, JSON, and SARIF reports.
95
- - Writes a GitHub Step Summary for pull request review.
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
- - Release created: <https://github.com/ChaoYue0307/mcp-guard-action/releases/tag/v0.4.0>
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.0
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
 
@@ -1,6 +1,6 @@
1
1
  # mcp-guard Scan Report
2
2
 
3
- Generated: 2026-05-10T12:58:03.265Z
3
+ Generated: 2026-05-10T13:31:24.377Z
4
4
 
5
5
  ## Summary
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mcp-guard",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Open-source CLI scanner for risky MCP server and AI agent tool configuration.",
5
5
  "type": "module",
6
6
  "homepage": "https://chaoyue0307.github.io/mcp-guard/",
@@ -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 12:58 UTC</strong><span>Generated</span></div>
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">
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "metadata": {
3
- "generatedAt": "2026-05-10T12:58:03.306Z",
3
+ "generatedAt": "2026-05-10T13:31:24.346Z",
4
4
  "cwd": ".",
5
5
  "home": "~",
6
- "toolVersion": "0.4.0"
6
+ "toolVersion": "0.4.2"
7
7
  },
8
8
  "scannedFiles": [
9
9
  "site/e2e/claude_desktop_config.json"
@@ -1,6 +1,6 @@
1
1
  # mcp-guard Scan Report
2
2
 
3
- Generated: 2026-05-10T12:58:03.279Z
3
+ Generated: 2026-05-10T13:31:24.329Z
4
4
 
5
5
  ## Summary
6
6
 
@@ -7,7 +7,7 @@
7
7
  "driver": {
8
8
  "name": "mcp-guard",
9
9
  "informationUri": "https://github.com/ChaoYue0307/mcp-guard",
10
- "semanticVersion": "0.4.0",
10
+ "semanticVersion": "0.4.2",
11
11
  "rules": [
12
12
  {
13
13
  "id": "MCP010",
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.0";
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
+ }