@baichen_yu/mcp-guard 0.3.0 → 0.3.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 +175 -63
- package/dist/cli.js +20 -8
- package/dist/mcp/jsonrpc.d.ts +16 -0
- package/dist/mcp/jsonrpc.js +128 -0
- package/dist/mcp/types.d.ts +3 -1
- package/dist/scan/scanner.d.ts +1 -1
- package/dist/scan/scanner.js +2 -0
- package/docs/cli.md +6 -4
- package/docs/index.md +32 -22
- package/docs/quickstart.md +17 -2
- package/docs/releasing.md +46 -1
- package/docs/testing.md +67 -0
- package/package.json +10 -5
package/README.md
CHANGED
|
@@ -1,58 +1,115 @@
|
|
|
1
1
|
# mcp-guard
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/@baichen_yu/mcp-guard)
|
|
5
|
-
[](./LICENSE)
|
|
3
|
+
<div align="center">
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+

|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
**Security auditing and policy gating for MCP servers (local + CI).**
|
|
8
|
+
Deterministic checks. Actionable findings. Reproducible reports.
|
|
9
|
+
|
|
10
|
+
[](./.github/workflows/ci.yml)
|
|
11
|
+
[](https://www.npmjs.com/package/@baichen_yu/mcp-guard)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](./package.json)
|
|
14
|
+
[](https://tomas-1226.github.io/mcp-guard/)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
10
19
|
|
|
11
20
|
> [!IMPORTANT]
|
|
12
|
-
> Remote mode supports **HTTP JSON-RPC
|
|
21
|
+
> Remote mode supports **HTTP JSON-RPC** (`--http`) and **SSE** (`--sse`, optional `--sse-post`).
|
|
22
|
+
|
|
23
|
+
## Why people use mcp-guard
|
|
24
|
+
|
|
25
|
+
- ✅ **Deterministic contract tests** (no fuzzy behavior, no mystery calls)
|
|
26
|
+
- ✅ **Policy gate in CI** (`--fail-on off|low|medium|high`)
|
|
27
|
+
- ✅ **Security-focused rule packs** with profile controls (`default`, `strict`, `paranoid`)
|
|
28
|
+
- ✅ **Review-friendly output formats** (`report.md`, `report.json`, `report.sarif`)
|
|
29
|
+
- ✅ **Config discovery + redaction** for common MCP client config layouts
|
|
30
|
+
|
|
31
|
+
### Quick stats
|
|
13
32
|
|
|
14
|
-
|
|
33
|
+
| Signal | Value |
|
|
34
|
+
|---|---:|
|
|
35
|
+
| Transports | STDIO + HTTP JSON-RPC + SSE |
|
|
36
|
+
| Report formats | Markdown, JSON, SARIF |
|
|
37
|
+
| Rule profiles | 3 |
|
|
38
|
+
| Contract scenarios | list, call, error shape, cancellation behavior, large payload, timeout |
|
|
39
|
+
| Registry modes | lint, verify, score |
|
|
15
40
|
|
|
16
|
-
|
|
17
|
-
- CLI command after install: `mcp-guard`
|
|
18
|
-
- first scoped publish: `npm publish --access public`
|
|
41
|
+
---
|
|
19
42
|
|
|
20
43
|
## 30-second quickstart
|
|
21
44
|
|
|
22
|
-
###
|
|
45
|
+
### 1) No install (`npx`)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @baichen_yu/mcp-guard audit \
|
|
49
|
+
--stdio "node /absolute/path/to/your-mcp-server.cjs" \
|
|
50
|
+
--out reports \
|
|
51
|
+
--fail-on off
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> [!TIP]
|
|
55
|
+
> `--stdio` runs in your **current working directory**. Use an absolute path or `cd` into the project that contains your server first.
|
|
56
|
+
|
|
57
|
+
### Local demo (from this repo root)
|
|
23
58
|
|
|
24
59
|
```bash
|
|
25
|
-
|
|
60
|
+
npm run fixtures:gen
|
|
61
|
+
npx @baichen_yu/mcp-guard audit \
|
|
62
|
+
--stdio "node fixtures/servers/hello-mcp-server/server.cjs" \
|
|
63
|
+
--out reports \
|
|
64
|
+
--fail-on off
|
|
26
65
|
```
|
|
27
66
|
|
|
28
|
-
###
|
|
67
|
+
### 2) Global install
|
|
29
68
|
|
|
30
69
|
```bash
|
|
31
70
|
npm i -g @baichen_yu/mcp-guard
|
|
32
71
|
mcp-guard --help
|
|
33
72
|
```
|
|
34
73
|
|
|
35
|
-
###
|
|
74
|
+
### 3) Package + command naming
|
|
36
75
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
- npm package: **`@baichen_yu/mcp-guard`**
|
|
77
|
+
- runtime CLI command: **`mcp-guard`**
|
|
78
|
+
- first scoped publish command: **`npm publish --access public`**
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Architecture
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart LR
|
|
86
|
+
CLI[mcp-guard CLI] --> T[Transport layer\nSTDIO HTTP SSE]
|
|
87
|
+
T --> RPC[JSON-RPC client]
|
|
88
|
+
RPC --> TESTS[Contract tests]
|
|
89
|
+
RPC --> RULES[Rules and profiles]
|
|
90
|
+
TESTS --> REPORTS[Reports\nMD JSON SARIF]
|
|
91
|
+
RULES --> REPORTS
|
|
92
|
+
REPORTS --> GATE[Policy gate fail-on]
|
|
93
|
+
GATE --> CI[CI and code scanning]
|
|
54
94
|
```
|
|
55
95
|
|
|
96
|
+
### Audit pipeline
|
|
97
|
+
|
|
98
|
+
```mermaid
|
|
99
|
+
flowchart TD
|
|
100
|
+
A[Start audit] --> B[Initialize session]
|
|
101
|
+
B --> C[List tools]
|
|
102
|
+
C --> D[Run contract suite]
|
|
103
|
+
C --> E[Run schema and security rules]
|
|
104
|
+
D --> F[Apply profile tuning]
|
|
105
|
+
E --> F
|
|
106
|
+
F --> G[Compute score]
|
|
107
|
+
G --> H[Emit reports]
|
|
108
|
+
H --> I[Exit by policy threshold]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
56
113
|
## Report preview
|
|
57
114
|
|
|
58
115
|
```text
|
|
@@ -63,67 +120,122 @@ jobs:
|
|
|
63
120
|
- Target: node fixtures/servers/hello-mcp-server/server.cjs (stdio)
|
|
64
121
|
```
|
|
65
122
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```mermaid
|
|
69
|
-
graph LR
|
|
70
|
-
CLI[mcp-guard CLI] --> T[Transports: stdio/http]
|
|
71
|
-
T --> RPC[JSON-RPC]
|
|
72
|
-
RPC --> RULES[Rules + Profiles]
|
|
73
|
-
RULES --> REP[Reports: md/json/sarif]
|
|
74
|
-
REP --> GATE[Policy Gate (--fail-on)]
|
|
75
|
-
GATE --> CI[CI / Code Scanning]
|
|
76
|
-
```
|
|
123
|
+
---
|
|
77
124
|
|
|
78
125
|
## Commands
|
|
79
126
|
|
|
80
127
|
```bash
|
|
128
|
+
# Validate / Test / Audit
|
|
81
129
|
mcp-guard validate --stdio "node server.cjs" --profile default --out reports
|
|
82
130
|
mcp-guard test --stdio "node server.cjs" --out reports
|
|
83
131
|
mcp-guard audit --stdio "node server.cjs" --profile strict --fail-on medium --sarif reports/report.sarif
|
|
132
|
+
|
|
133
|
+
# Remote audit (HTTP JSON-RPC)
|
|
84
134
|
mcp-guard audit --http "http://127.0.0.1:4010" --timeout-ms 30000 --fail-on off
|
|
135
|
+
|
|
136
|
+
# Remote audit (SSE stream + POST endpoint)
|
|
137
|
+
mcp-guard audit --sse "http://127.0.0.1:4013/sse" --sse-post "http://127.0.0.1:4013/message" --timeout-ms 30000 --fail-on off
|
|
138
|
+
|
|
139
|
+
# Config scan
|
|
85
140
|
mcp-guard scan --repo . --format md --out reports
|
|
141
|
+
|
|
142
|
+
# Registry checks
|
|
86
143
|
mcp-guard registry lint registry/servers.yaml
|
|
87
144
|
mcp-guard registry verify registry/servers.yaml --sample 5
|
|
88
145
|
mcp-guard registry score registry/servers.yaml
|
|
89
146
|
```
|
|
90
147
|
|
|
91
|
-
|
|
148
|
+
---
|
|
92
149
|
|
|
93
|
-
|
|
94
|
-
- One-time setup: GitHub repo **Settings → Pages → Source → GitHub Actions**
|
|
95
|
-
- Deploy workflow: `.github/workflows/deploy-pages.yml`
|
|
150
|
+
## CI integration (drop-in)
|
|
96
151
|
|
|
97
|
-
|
|
152
|
+
```yaml
|
|
153
|
+
jobs:
|
|
154
|
+
mcp-audit:
|
|
155
|
+
runs-on: ubuntu-latest
|
|
156
|
+
permissions:
|
|
157
|
+
security-events: write
|
|
158
|
+
actions: read
|
|
159
|
+
contents: read
|
|
160
|
+
steps:
|
|
161
|
+
- uses: actions/checkout@v4
|
|
162
|
+
- uses: actions/setup-node@v4
|
|
163
|
+
with:
|
|
164
|
+
node-version: 20
|
|
165
|
+
- uses: ./.github/actions/mcp-guard
|
|
166
|
+
with:
|
|
167
|
+
stdio_command: node fixtures/servers/hello-mcp-server/server.cjs
|
|
168
|
+
fail_on: high
|
|
169
|
+
```
|
|
98
170
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Automated releases
|
|
174
|
+
|
|
175
|
+
- On each push to `main`, `.github/workflows/release.yml` now:
|
|
176
|
+
- runs lint/test/build
|
|
177
|
+
- computes the next available version above local and npm latest
|
|
178
|
+
- builds release assets (package tarball + compiled `dist` archive)
|
|
179
|
+
- creates/updates a GitHub Release with GitHub generated (auto/AI-style) release notes + uploaded assets
|
|
180
|
+
- publishes the new package to npm (requires `NPM_TOKEN`)
|
|
181
|
+
- For npm publishing in CI, set `NPM_TOKEN` to an **npm Automation token** (no interactive password/OTP required).
|
|
182
|
+
|
|
183
|
+
## Docs site (GitHub Pages)
|
|
184
|
+
|
|
185
|
+
- URL pattern: `https://<owner>.github.io/mcp-guard/`
|
|
186
|
+
- Current docs: https://tomas-1226.github.io/mcp-guard/
|
|
187
|
+
- One-time setup: **Settings → Pages → Source → GitHub Actions**
|
|
188
|
+
|
|
189
|
+
---
|
|
102
190
|
|
|
103
191
|
## Troubleshooting
|
|
104
192
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
193
|
+
<details>
|
|
194
|
+
<summary><strong>Publishing ENOENT / package.json not found</strong></summary>
|
|
195
|
+
|
|
196
|
+
Run `npm publish` from the project root (the directory containing `package.json`).
|
|
197
|
+
|
|
198
|
+
</details>
|
|
199
|
+
|
|
200
|
+
<details>
|
|
201
|
+
<summary><strong>HTTP target fails</strong></summary>
|
|
202
|
+
|
|
203
|
+
Confirm endpoint supports JSON-RPC via HTTP POST and increase `--timeout-ms` if startup is slow.
|
|
109
204
|
|
|
110
|
-
|
|
205
|
+
</details>
|
|
111
206
|
|
|
112
|
-
|
|
207
|
+
<details>
|
|
208
|
+
<summary><strong>Windows command quoting</strong></summary>
|
|
113
209
|
|
|
114
|
-
|
|
210
|
+
Wrap `--stdio` values in double quotes.
|
|
115
211
|
|
|
116
|
-
|
|
212
|
+
</details>
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Release helper
|
|
217
|
+
|
|
218
|
+
Build release artifacts locally/offline (after one online `npm ci`):
|
|
117
219
|
|
|
118
220
|
```bash
|
|
119
|
-
|
|
221
|
+
npm run release:offline
|
|
120
222
|
```
|
|
121
223
|
|
|
122
|
-
|
|
224
|
+
---
|
|
123
225
|
|
|
124
|
-
|
|
125
|
-
|
|
226
|
+
## Links
|
|
227
|
+
|
|
228
|
+
- Docs: https://tomas-1226.github.io/mcp-guard/
|
|
229
|
+
- GitHub: https://github.com/TomAs-1226/mcp-guard
|
|
230
|
+
- npm: https://www.npmjs.com/package/@baichen_yu/mcp-guard
|
|
126
231
|
|
|
127
232
|
## License
|
|
128
233
|
|
|
129
|
-
MIT. See [
|
|
234
|
+
MIT. See [LICENSE](LICENSE).
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
### npm package run check
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npm run npm:test-run
|
|
241
|
+
```
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
|
-
import { HttpJsonRpcClient, StdioJsonRpcClient } from './mcp/jsonrpc.js';
|
|
5
|
+
import { HttpJsonRpcClient, SseJsonRpcClient, StdioJsonRpcClient } from './mcp/jsonrpc.js';
|
|
6
6
|
import { StdioTransport } from './mcp/transport_stdio.js';
|
|
7
7
|
import { writeJsonReport } from './report/json.js';
|
|
8
8
|
import { writeMarkdownReport } from './report/markdown.js';
|
|
@@ -40,19 +40,27 @@ function shouldFailByPolicy(findings, failOn) {
|
|
|
40
40
|
return findings.some((finding) => severityRank[finding.severity] >= rank[failOn]);
|
|
41
41
|
}
|
|
42
42
|
async function buildClient(options) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const selected = [options.stdio, options.http, options.sse].filter(Boolean).length;
|
|
44
|
+
if (selected === 0)
|
|
45
|
+
throw new Error('One of --stdio, --http, or --sse is required.');
|
|
46
|
+
if (selected > 1)
|
|
47
|
+
throw new Error('Use only one transport: --stdio, --http, or --sse.');
|
|
47
48
|
if (options.stdio) {
|
|
48
49
|
const transport = new StdioTransport();
|
|
49
50
|
await transport.start({ stdioCommand: options.stdio, silent: options.silent });
|
|
50
51
|
return { client: new StdioJsonRpcClient(transport, options.timeoutMs), target: options.stdio, transport: 'stdio' };
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
if (options.http) {
|
|
54
|
+
return { client: new HttpJsonRpcClient(options.http, options.timeoutMs), target: options.http, transport: 'http' };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
client: new SseJsonRpcClient(options.sse, options.timeoutMs, options.ssePost ?? options.sse),
|
|
58
|
+
target: options.sse,
|
|
59
|
+
transport: 'sse'
|
|
60
|
+
};
|
|
53
61
|
}
|
|
54
62
|
async function executeSuite(params) {
|
|
55
|
-
const { client, target, transport } = await buildClient({ stdio: params.stdio, http: params.http, timeoutMs: params.timeoutMs, silent: false });
|
|
63
|
+
const { client, target, transport } = await buildClient({ stdio: params.stdio, http: params.http, sse: params.sse, ssePost: params.ssePost, timeoutMs: params.timeoutMs, silent: false });
|
|
56
64
|
const findings = [];
|
|
57
65
|
let tools = [];
|
|
58
66
|
const tests = [];
|
|
@@ -110,13 +118,15 @@ async function executeSuite(params) {
|
|
|
110
118
|
}
|
|
111
119
|
program
|
|
112
120
|
.name('mcp-guard')
|
|
113
|
-
.description('Validate, test, and security-audit MCP servers over STDIO or
|
|
121
|
+
.description('Validate, test, and security-audit MCP servers over STDIO, HTTP JSON-RPC, or SSE + JSON-RPC')
|
|
114
122
|
.showHelpAfterError();
|
|
115
123
|
for (const commandName of ['validate', 'test', 'audit']) {
|
|
116
124
|
program.command(commandName)
|
|
117
125
|
.description(`${commandName} an MCP server`)
|
|
118
126
|
.option('--stdio <cmd>', 'Command used to launch MCP server over stdio')
|
|
119
127
|
.option('--http <url>', 'HTTP JSON-RPC endpoint URL')
|
|
128
|
+
.option('--sse <url>', 'SSE endpoint URL for responses')
|
|
129
|
+
.option('--sse-post <url>', 'HTTP POST URL used to send JSON-RPC requests (defaults to --sse)')
|
|
120
130
|
.option('--out <dir>', 'Output directory', 'reports')
|
|
121
131
|
.option('--sarif <file>', 'SARIF output file (mainly for audit)', 'reports/report.sarif')
|
|
122
132
|
.option('--profile <profile>', 'Rule profile: default|strict|paranoid', parseProfile, 'default')
|
|
@@ -128,6 +138,8 @@ for (const commandName of ['validate', 'test', 'audit']) {
|
|
|
128
138
|
const code = await executeSuite({
|
|
129
139
|
stdio: options.stdio,
|
|
130
140
|
http: options.http,
|
|
141
|
+
sse: options.sse,
|
|
142
|
+
ssePost: options.ssePost,
|
|
131
143
|
outDir: options.out,
|
|
132
144
|
runTests: doTests,
|
|
133
145
|
runAudit: doAudit,
|
package/dist/mcp/jsonrpc.d.ts
CHANGED
|
@@ -20,4 +20,20 @@ export declare class HttpJsonRpcClient implements RpcClient {
|
|
|
20
20
|
normalizeError(error: unknown): NormalizedError;
|
|
21
21
|
close(): Promise<void>;
|
|
22
22
|
}
|
|
23
|
+
export declare class SseJsonRpcClient implements RpcClient {
|
|
24
|
+
private readonly streamUrl;
|
|
25
|
+
private readonly timeoutMs;
|
|
26
|
+
private readonly postUrl;
|
|
27
|
+
private nextId;
|
|
28
|
+
private pending;
|
|
29
|
+
private streamController?;
|
|
30
|
+
private readerPromise?;
|
|
31
|
+
constructor(streamUrl: string, timeoutMs?: number, postUrl?: string);
|
|
32
|
+
request<T>(method: string, params?: unknown, timeoutOverrideMs?: number): Promise<T>;
|
|
33
|
+
normalizeError(error: unknown): NormalizedError;
|
|
34
|
+
close(): Promise<void>;
|
|
35
|
+
private ensureStream;
|
|
36
|
+
private openAndReadStream;
|
|
37
|
+
private resolvePending;
|
|
38
|
+
}
|
|
23
39
|
export declare function normalizeError(error: unknown): NormalizedError;
|
package/dist/mcp/jsonrpc.js
CHANGED
|
@@ -101,6 +101,134 @@ export class HttpJsonRpcClient {
|
|
|
101
101
|
await Promise.resolve();
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
export class SseJsonRpcClient {
|
|
105
|
+
streamUrl;
|
|
106
|
+
timeoutMs;
|
|
107
|
+
postUrl;
|
|
108
|
+
nextId = 1;
|
|
109
|
+
pending = new Map();
|
|
110
|
+
streamController;
|
|
111
|
+
readerPromise;
|
|
112
|
+
constructor(streamUrl, timeoutMs = 5000, postUrl = streamUrl) {
|
|
113
|
+
this.streamUrl = streamUrl;
|
|
114
|
+
this.timeoutMs = timeoutMs;
|
|
115
|
+
this.postUrl = postUrl;
|
|
116
|
+
}
|
|
117
|
+
async request(method, params, timeoutOverrideMs) {
|
|
118
|
+
await this.ensureStream();
|
|
119
|
+
const id = this.nextId++;
|
|
120
|
+
const payload = { jsonrpc: '2.0', id, method, params };
|
|
121
|
+
return new Promise(async (resolve, reject) => {
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
this.pending.delete(id);
|
|
124
|
+
reject(new Error(`Request timed out: ${method}`));
|
|
125
|
+
}, timeoutOverrideMs ?? this.timeoutMs);
|
|
126
|
+
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
127
|
+
try {
|
|
128
|
+
const response = await fetch(this.postUrl, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'content-type': 'application/json' },
|
|
131
|
+
body: JSON.stringify(payload)
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`SSE POST failed: ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
137
|
+
if (contentType.includes('application/json')) {
|
|
138
|
+
const parsed = (await response.json());
|
|
139
|
+
this.resolvePending(parsed);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const pending = this.pending.get(id);
|
|
144
|
+
if (pending) {
|
|
145
|
+
clearTimeout(pending.timer);
|
|
146
|
+
this.pending.delete(id);
|
|
147
|
+
pending.reject(error);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
normalizeError(error) {
|
|
153
|
+
return normalizeError(error);
|
|
154
|
+
}
|
|
155
|
+
async close() {
|
|
156
|
+
this.streamController?.abort();
|
|
157
|
+
await this.readerPromise?.catch(() => undefined);
|
|
158
|
+
for (const [id, pending] of this.pending) {
|
|
159
|
+
clearTimeout(pending.timer);
|
|
160
|
+
pending.reject(new Error('SSE client closed'));
|
|
161
|
+
this.pending.delete(id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async ensureStream() {
|
|
165
|
+
if (this.readerPromise)
|
|
166
|
+
return;
|
|
167
|
+
this.streamController = new AbortController();
|
|
168
|
+
this.readerPromise = this.openAndReadStream(this.streamController.signal).catch((error) => {
|
|
169
|
+
for (const [id, pending] of this.pending) {
|
|
170
|
+
clearTimeout(pending.timer);
|
|
171
|
+
pending.reject(error);
|
|
172
|
+
this.pending.delete(id);
|
|
173
|
+
}
|
|
174
|
+
this.readerPromise = undefined;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async openAndReadStream(signal) {
|
|
178
|
+
const response = await fetch(this.streamUrl, {
|
|
179
|
+
method: 'GET',
|
|
180
|
+
headers: { Accept: 'text/event-stream' },
|
|
181
|
+
signal
|
|
182
|
+
});
|
|
183
|
+
if (!response.ok || !response.body) {
|
|
184
|
+
throw new Error(`Failed to open SSE stream: ${response.status}`);
|
|
185
|
+
}
|
|
186
|
+
const decoder = new TextDecoder();
|
|
187
|
+
const reader = response.body.getReader();
|
|
188
|
+
let buffer = '';
|
|
189
|
+
let dataLines = [];
|
|
190
|
+
while (true) {
|
|
191
|
+
const { value, done } = await reader.read();
|
|
192
|
+
if (done)
|
|
193
|
+
break;
|
|
194
|
+
buffer += decoder.decode(value, { stream: true });
|
|
195
|
+
let idx = buffer.indexOf('\n');
|
|
196
|
+
while (idx >= 0) {
|
|
197
|
+
const line = buffer.slice(0, idx).replace(/\r$/, '');
|
|
198
|
+
buffer = buffer.slice(idx + 1);
|
|
199
|
+
if (line.startsWith('data:')) {
|
|
200
|
+
dataLines.push(line.slice(5).trimStart());
|
|
201
|
+
}
|
|
202
|
+
else if (line === '') {
|
|
203
|
+
if (dataLines.length > 0) {
|
|
204
|
+
const payload = dataLines.join('\n');
|
|
205
|
+
dataLines = [];
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(payload);
|
|
208
|
+
this.resolvePending(parsed);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// ignore malformed events
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
idx = buffer.indexOf('\n');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
resolvePending(parsed) {
|
|
220
|
+
const pending = this.pending.get(parsed.id);
|
|
221
|
+
if (!pending)
|
|
222
|
+
return;
|
|
223
|
+
clearTimeout(pending.timer);
|
|
224
|
+
this.pending.delete(parsed.id);
|
|
225
|
+
if ('error' in parsed) {
|
|
226
|
+
pending.reject(parsed.error);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
pending.resolve(parsed.result);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
104
232
|
export function normalizeError(error) {
|
|
105
233
|
if (typeof error === 'object' && error !== null && 'code' in error && 'message' in error) {
|
|
106
234
|
const err = error;
|
package/dist/mcp/types.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface ToolDescriptor {
|
|
|
29
29
|
export interface ServerConfig {
|
|
30
30
|
stdioCommand?: string;
|
|
31
31
|
httpUrl?: string;
|
|
32
|
+
sseUrl?: string;
|
|
33
|
+
ssePostUrl?: string;
|
|
32
34
|
cwd?: string;
|
|
33
35
|
env?: Record<string, string>;
|
|
34
36
|
timeoutMs?: number;
|
|
@@ -52,7 +54,7 @@ export interface TestResult {
|
|
|
52
54
|
export interface Report {
|
|
53
55
|
server: {
|
|
54
56
|
target: string;
|
|
55
|
-
transport: 'stdio' | 'http';
|
|
57
|
+
transport: 'stdio' | 'http' | 'sse';
|
|
56
58
|
protocolVersion?: string;
|
|
57
59
|
};
|
|
58
60
|
tools: ToolDescriptor[];
|
package/dist/scan/scanner.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export interface DiscoveredServer {
|
|
|
2
2
|
source: 'claude_desktop' | 'cursor';
|
|
3
3
|
path: string;
|
|
4
4
|
name: string;
|
|
5
|
-
transport: 'stdio' | 'http' | 'unknown';
|
|
5
|
+
transport: 'stdio' | 'http' | 'sse' | 'unknown';
|
|
6
6
|
command?: string;
|
|
7
7
|
rawCommand?: string;
|
|
8
8
|
permissions?: string[];
|
package/dist/scan/scanner.js
CHANGED
|
@@ -23,6 +23,8 @@ function redact(text) {
|
|
|
23
23
|
function detectTransport(server) {
|
|
24
24
|
if (typeof server.command === 'string')
|
|
25
25
|
return 'stdio';
|
|
26
|
+
if (typeof server.sseUrl === 'string' || typeof server.sse === 'string')
|
|
27
|
+
return 'sse';
|
|
26
28
|
if (typeof server.url === 'string' || typeof server.httpUrl === 'string')
|
|
27
29
|
return 'http';
|
|
28
30
|
return 'unknown';
|
package/docs/cli.md
CHANGED
|
@@ -16,9 +16,9 @@ mcp-guard --help
|
|
|
16
16
|
|
|
17
17
|
## Core commands
|
|
18
18
|
|
|
19
|
-
- `mcp-guard validate --stdio <cmd>|--http <url> [--profile default|strict|paranoid] [--out reports] [--timeout-ms 30000]`
|
|
20
|
-
- `mcp-guard test --stdio <cmd>|--http <url> [--out reports] [--timeout-ms 30000]`
|
|
21
|
-
- `mcp-guard audit --stdio <cmd>|--http <url> [--profile ...] [--fail-on off|low|medium|high] [--sarif reports/report.sarif]`
|
|
19
|
+
- `mcp-guard validate --stdio <cmd>|--http <url>|--sse <url> [--sse-post <url>] [--profile default|strict|paranoid] [--out reports] [--timeout-ms 30000]`
|
|
20
|
+
- `mcp-guard test --stdio <cmd>|--http <url>|--sse <url> [--sse-post <url>] [--out reports] [--timeout-ms 30000]`
|
|
21
|
+
- `mcp-guard audit --stdio <cmd>|--http <url>|--sse <url> [--sse-post <url>] [--profile ...] [--fail-on off|low|medium|high] [--sarif reports/report.sarif]`
|
|
22
22
|
|
|
23
23
|
## Scan and registry commands
|
|
24
24
|
|
|
@@ -35,4 +35,6 @@ mcp-guard --help
|
|
|
35
35
|
|
|
36
36
|
## Remote transport support
|
|
37
37
|
|
|
38
|
-
`--http` supports HTTP JSON-RPC (POST)
|
|
38
|
+
`--http` supports HTTP JSON-RPC (POST).
|
|
39
|
+
|
|
40
|
+
`--sse` supports SSE response streams (GET) with request POSTs sent to `--sse-post` (or to `--sse` when omitted).
|
package/docs/index.md
CHANGED
|
@@ -3,39 +3,39 @@ layout: home
|
|
|
3
3
|
|
|
4
4
|
hero:
|
|
5
5
|
name: "mcp-guard"
|
|
6
|
-
text: "
|
|
7
|
-
tagline: "
|
|
6
|
+
text: "Security gating for MCP servers, from local dev to CI"
|
|
7
|
+
tagline: "Validate protocol contracts, test runtime behavior, and enforce policy with reproducible reports."
|
|
8
8
|
image:
|
|
9
9
|
src: /brand-mark.svg
|
|
10
10
|
alt: mcp-guard
|
|
11
11
|
actions:
|
|
12
12
|
- theme: brand
|
|
13
|
-
text: 30
|
|
13
|
+
text: Start in 30 seconds
|
|
14
14
|
link: /quickstart
|
|
15
|
+
- theme: alt
|
|
16
|
+
text: Run tests
|
|
17
|
+
link: /testing
|
|
15
18
|
- theme: alt
|
|
16
19
|
text: CLI reference
|
|
17
20
|
link: /cli
|
|
18
|
-
- theme: alt
|
|
19
|
-
text: GitHub Action
|
|
20
|
-
link: /github-action
|
|
21
21
|
|
|
22
22
|
features:
|
|
23
23
|
- icon: 🧪
|
|
24
|
-
title: Deterministic
|
|
25
|
-
details: Fixed checks for tools/list, tool
|
|
24
|
+
title: Deterministic contract checks
|
|
25
|
+
details: Fixed checks for tools/list, tool-call behavior, malformed payloads, cancellation behavior, large responses, and timeout boundaries.
|
|
26
26
|
- icon: 🛡️
|
|
27
27
|
title: Policy gate built-in
|
|
28
|
-
details: Use
|
|
28
|
+
details: Use profile severity and --fail-on thresholds to enforce guardrails in CI without custom glue code.
|
|
29
29
|
- icon: 📄
|
|
30
30
|
title: Reproducible outputs
|
|
31
|
-
details: Markdown + JSON + SARIF
|
|
31
|
+
details: Generate Markdown + JSON + SARIF artifacts designed for pull-request review and code scanning.
|
|
32
32
|
- icon: 🚦
|
|
33
|
-
title:
|
|
34
|
-
details: Local stdio
|
|
33
|
+
title: Three transport modes
|
|
34
|
+
details: Local stdio plus remote HTTP JSON-RPC and SSE support with bounded retries and timeouts.
|
|
35
35
|
---
|
|
36
36
|
|
|
37
37
|
> [!IMPORTANT]
|
|
38
|
-
> Remote mode
|
|
38
|
+
> Remote mode supports **HTTP JSON-RPC** (`--http`) and **SSE** (`--sse`).
|
|
39
39
|
|
|
40
40
|
<div class="stats-grid">
|
|
41
41
|
<div class="stat-card"><h4>Profiles</h4><p><code>default</code> · <code>strict</code> · <code>paranoid</code></p></div>
|
|
@@ -43,21 +43,31 @@ features:
|
|
|
43
43
|
<div class="stat-card"><h4>Policy</h4><p><code>--fail-on off|low|medium|high</code></p></div>
|
|
44
44
|
</div>
|
|
45
45
|
|
|
46
|
-
##
|
|
46
|
+
## Typical workflow
|
|
47
47
|
|
|
48
|
-
<div class="
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
<div class="workflow-grid">
|
|
49
|
+
<div class="workflow-card">
|
|
50
|
+
<h4>1) Validate quickly</h4>
|
|
51
|
+
<p>Run checks locally against your MCP server before opening a PR.</p>
|
|
52
|
+
<code>mcp-guard validate --stdio "node server.cjs"</code>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="workflow-card">
|
|
55
|
+
<h4>2) Test behavior</h4>
|
|
56
|
+
<p>Execute deterministic test probes and emit machine-readable reports.</p>
|
|
57
|
+
<code>mcp-guard test --stdio "node server.cjs"</code>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="workflow-card">
|
|
60
|
+
<h4>3) Gate in CI</h4>
|
|
61
|
+
<p>Use <code>audit</code> with SARIF and severity thresholds to block risky changes.</p>
|
|
62
|
+
<code>mcp-guard audit --fail-on medium --sarif reports/report.sarif</code>
|
|
63
|
+
</div>
|
|
54
64
|
</div>
|
|
55
65
|
|
|
56
66
|
## Architecture
|
|
57
67
|
|
|
58
68
|
```mermaid
|
|
59
69
|
graph LR
|
|
60
|
-
CLI[mcp-guard CLI] --> T[Transports: stdio/http]
|
|
70
|
+
CLI[mcp-guard CLI] --> T[Transports: stdio/http/sse]
|
|
61
71
|
T --> RPC[JSON-RPC]
|
|
62
72
|
RPC --> RULES[Rules + Profiles]
|
|
63
73
|
RULES --> REP[Reports: md/json/sarif]
|
|
@@ -71,5 +81,5 @@ graph LR
|
|
|
71
81
|
2. Upload SARIF so findings show in security dashboards.
|
|
72
82
|
3. Gate merges on reproducible report output.
|
|
73
83
|
|
|
74
|
-
- GitHub: https://github.com/TomAs-1226/
|
|
84
|
+
- GitHub: https://github.com/TomAs-1226/mcp-guard
|
|
75
85
|
- npm: https://www.npmjs.com/package/@baichen_yu/mcp-guard
|
package/docs/quickstart.md
CHANGED
|
@@ -27,8 +27,23 @@ npm publish --access public
|
|
|
27
27
|
|
|
28
28
|
1. Enable **Settings → Pages → Source → GitHub Actions** once.
|
|
29
29
|
2. Docs deploy from `.github/workflows/deploy-pages.yml`.
|
|
30
|
-
3. Expected URL
|
|
30
|
+
3. Expected URL after repo rename to `mcp-guard`: `https://<owner>.github.io/mcp-guard/`.
|
|
31
|
+
4. If the site renders as plain text or a blank page, confirm docs base path is set to `/<repo-name>/`.
|
|
31
32
|
|
|
32
33
|
## Remote mode note
|
|
33
34
|
|
|
34
|
-
Remote mode supports **HTTP JSON-RPC
|
|
35
|
+
Remote mode supports **HTTP JSON-RPC** (`--http`) and **SSE** (`--sse`, optional `--sse-post`).
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## SSE quick check
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
node fixtures/servers/sse-mcp-server/server.cjs
|
|
42
|
+
# in another shell
|
|
43
|
+
npx @baichen_yu/mcp-guard audit --sse "http://127.0.0.1:4013/sse" --sse-post "http://127.0.0.1:4013/message" --out reports --fail-on off
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Release auth note
|
|
48
|
+
|
|
49
|
+
For automatic npm releases in GitHub Actions, create an **npm Automation token** and store it as repository secret `NPM_TOKEN`. This avoids interactive password/OTP prompts during CI publish.
|
package/docs/releasing.md
CHANGED
|
@@ -36,4 +36,49 @@ git push origin v0.3.0
|
|
|
36
36
|
Then create a GitHub Release and include:
|
|
37
37
|
- key highlights
|
|
38
38
|
- migration note (formerly mcp-doctor)
|
|
39
|
-
-
|
|
39
|
+
- transport note (HTTP JSON-RPC + SSE supported)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Automated release pipeline
|
|
43
|
+
|
|
44
|
+
On pushes to `main`, `.github/workflows/release.yml` will:
|
|
45
|
+
|
|
46
|
+
1. Run `npm run lint`, `npm test`, and `npm run build`.
|
|
47
|
+
2. Compute the next available version above both local `package.json` and npm latest, then bump to that version.
|
|
48
|
+
3. Push commit + tag back to GitHub.
|
|
49
|
+
4. Build release assets (`npm pack` tarball + compiled `dist` archive).
|
|
50
|
+
5. Publish to npm with provenance.
|
|
51
|
+
6. Create/update a GitHub Release with generated release notes and uploaded assets.
|
|
52
|
+
|
|
53
|
+
Required secret: `NPM_TOKEN` with publish permission for `@baichen_yu/mcp-guard`.
|
|
54
|
+
|
|
55
|
+
Use an **npm Automation token** (recommended) so the workflow can publish without interactive password/OTP prompts.
|
|
56
|
+
|
|
57
|
+
- npm: create token at <https://www.npmjs.com/settings/tokens>
|
|
58
|
+
- GitHub: add it as repository secret `NPM_TOKEN`
|
|
59
|
+
- workflow preflight runs `npm whoami` to confirm auth before version bump/publish
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Offline release package build
|
|
63
|
+
|
|
64
|
+
After dependencies are installed once (`npm ci` while online), you can build the release tarball completely offline:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm run release:offline
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If `node_modules` is missing, the script exits with guidance.
|
|
71
|
+
|
|
72
|
+
## npm package test-run command
|
|
73
|
+
|
|
74
|
+
Use this to verify the published package entrypoint works:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run npm:test-run
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
The workflow also supports manual trigger via `workflow_dispatch` in GitHub Actions.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
If a release tag already exists (for example, from a re-run), the workflow updates that release and re-uploads assets with `--clobber` instead of failing.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Testing options
|
|
2
|
+
|
|
3
|
+
Use these commands depending on what you want to verify.
|
|
4
|
+
|
|
5
|
+
## 1) Unit/integration test suite (Vitest)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Runs the project test suite and regenerates fixtures first (`pretest`).
|
|
12
|
+
|
|
13
|
+
## 2) TypeScript compile checks (no emit)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run lint
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Confirms the codebase type-checks cleanly.
|
|
20
|
+
|
|
21
|
+
## 3) Production build
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm run build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Compiles CLI sources to `dist/`.
|
|
28
|
+
|
|
29
|
+
## 4) Docs build (GitHub Pages artifact parity)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm run docs:build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Builds the VitePress static site exactly like the Pages workflow.
|
|
36
|
+
|
|
37
|
+
## 5) Example end-to-end CLI run
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx tsx src/cli.ts audit --stdio "node fixtures/servers/hello-mcp-server/server.cjs" --out reports --fail-on off
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This is a practical smoke test that exercises transport, rules, and report generation.
|
|
44
|
+
|
|
45
|
+
## Minimal local validation flow
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm run lint && npm test && npm run build && npm run docs:build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## SSE smoke test
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node fixtures/servers/sse-mcp-server/server.cjs
|
|
56
|
+
# new terminal
|
|
57
|
+
node dist/cli.js audit --sse "http://127.0.0.1:4013/sse" --sse-post "http://127.0.0.1:4013/message" --out reports --fail-on off
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## Published npm package smoke test
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm run npm:test-run
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This executes the package from npm and prints CLI help to verify install/entrypoint integrity.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baichen_yu/mcp-guard",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Security auditing and policy gating for MCP servers (STDIO/HTTP) with Markdown + SARIF reports",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,13 +11,18 @@
|
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"prepack": "npm run build",
|
|
14
15
|
"dev": "tsx src/cli.ts",
|
|
16
|
+
"pretest": "npm run fixtures:gen",
|
|
15
17
|
"test": "vitest run",
|
|
16
18
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
17
19
|
"fixtures:gen": "tsx scripts/gen-fixtures.ts",
|
|
18
20
|
"docs:dev": "vitepress dev docs",
|
|
19
21
|
"docs:build": "vitepress build docs",
|
|
20
|
-
"docs:preview": "vitepress preview docs"
|
|
22
|
+
"docs:preview": "vitepress preview docs",
|
|
23
|
+
"prepublishOnly": "npm run lint && npm test",
|
|
24
|
+
"release:offline": "bash scripts/build-release-local.sh",
|
|
25
|
+
"npm:test-run": "PKG=$(npm pack --silent | tail -n 1) && npx --yes --package \"./$PKG\" mcp-guard --help && rm -f \"$PKG\""
|
|
21
26
|
},
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"commander": "^12.1.0",
|
|
@@ -41,11 +46,11 @@
|
|
|
41
46
|
],
|
|
42
47
|
"repository": {
|
|
43
48
|
"type": "git",
|
|
44
|
-
"url": "https://github.com/TomAs-1226/
|
|
49
|
+
"url": "https://github.com/TomAs-1226/mcp-guard.git"
|
|
45
50
|
},
|
|
46
|
-
"homepage": "https://tomas-1226.github.io/
|
|
51
|
+
"homepage": "https://tomas-1226.github.io/mcp-guard/",
|
|
47
52
|
"bugs": {
|
|
48
|
-
"url": "https://github.com/TomAs-1226/
|
|
53
|
+
"url": "https://github.com/TomAs-1226/mcp-guard/issues"
|
|
49
54
|
},
|
|
50
55
|
"publishConfig": {
|
|
51
56
|
"access": "public"
|