@baichen_yu/mcp-guard 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/RELEASE.md +21 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +257 -0
  6. package/dist/mcp/jsonrpc.d.ts +23 -0
  7. package/dist/mcp/jsonrpc.js +124 -0
  8. package/dist/mcp/transport_stdio.d.ts +10 -0
  9. package/dist/mcp/transport_stdio.js +69 -0
  10. package/dist/mcp/types.d.ts +75 -0
  11. package/dist/mcp/types.js +1 -0
  12. package/dist/registry/validator.d.ts +20 -0
  13. package/dist/registry/validator.js +98 -0
  14. package/dist/report/json.d.ts +1 -0
  15. package/dist/report/json.js +8 -0
  16. package/dist/report/markdown.d.ts +2 -0
  17. package/dist/report/markdown.js +61 -0
  18. package/dist/report/sarif.d.ts +2 -0
  19. package/dist/report/sarif.js +33 -0
  20. package/dist/scan/scanner.d.ts +16 -0
  21. package/dist/scan/scanner.js +86 -0
  22. package/dist/security/profiles.d.ts +6 -0
  23. package/dist/security/profiles.js +25 -0
  24. package/dist/security/rules/path_traversal.d.ts +2 -0
  25. package/dist/security/rules/path_traversal.js +24 -0
  26. package/dist/security/rules/raw_args.d.ts +2 -0
  27. package/dist/security/rules/raw_args.js +24 -0
  28. package/dist/security/rules/shell_injection.d.ts +2 -0
  29. package/dist/security/rules/shell_injection.js +23 -0
  30. package/dist/security/scorer.d.ts +5 -0
  31. package/dist/security/scorer.js +4 -0
  32. package/dist/tests/contract/call_tool.d.ts +2 -0
  33. package/dist/tests/contract/call_tool.js +14 -0
  34. package/dist/tests/contract/cancellation.d.ts +2 -0
  35. package/dist/tests/contract/cancellation.js +22 -0
  36. package/dist/tests/contract/error_shapes.d.ts +2 -0
  37. package/dist/tests/contract/error_shapes.js +25 -0
  38. package/dist/tests/contract/large_payload.d.ts +2 -0
  39. package/dist/tests/contract/large_payload.js +15 -0
  40. package/dist/tests/contract/list_tools.d.ts +5 -0
  41. package/dist/tests/contract/list_tools.js +14 -0
  42. package/dist/tests/contract/timeout.d.ts +2 -0
  43. package/dist/tests/contract/timeout.js +25 -0
  44. package/dist/validate/schema_lints.d.ts +2 -0
  45. package/dist/validate/schema_lints.js +65 -0
  46. package/docs/assets/demo.gif +1 -0
  47. package/docs/cli.md +38 -0
  48. package/docs/github-action.md +23 -0
  49. package/docs/index.md +75 -0
  50. package/docs/quickstart.md +34 -0
  51. package/docs/releasing.md +39 -0
  52. package/docs/rules.md +18 -0
  53. package/docs/security-model.md +14 -0
  54. package/package.json +64 -0
@@ -0,0 +1,14 @@
1
+ export async function runListToolsTest(client) {
2
+ const start = Date.now();
3
+ const listed = (await client.request('tools/list')).tools;
4
+ const passed = Array.isArray(listed) && listed.length > 0;
5
+ return {
6
+ result: {
7
+ name: 'list_tools',
8
+ passed,
9
+ durationMs: Date.now() - start,
10
+ details: passed ? `Listed ${listed.length} tools` : 'No tools returned'
11
+ },
12
+ tools: listed
13
+ };
14
+ }
@@ -0,0 +1,2 @@
1
+ import { RpcClient, TestResult } from '../../mcp/types.js';
2
+ export declare function runTimeoutTest(client: RpcClient): Promise<TestResult>;
@@ -0,0 +1,25 @@
1
+ export async function runTimeoutTest(client) {
2
+ const start = Date.now();
3
+ try {
4
+ await client.request('tools/call', {
5
+ name: 'sleep',
6
+ arguments: { ms: 300 }
7
+ }, 30);
8
+ return {
9
+ name: 'deterministic_timeout',
10
+ passed: false,
11
+ durationMs: Date.now() - start,
12
+ details: 'Expected timeout did not occur'
13
+ };
14
+ }
15
+ catch (error) {
16
+ const normalized = client.normalizeError(error);
17
+ const passed = /timed out|aborted|abort/i.test(normalized.message);
18
+ return {
19
+ name: 'deterministic_timeout',
20
+ passed,
21
+ durationMs: Date.now() - start,
22
+ details: normalized.message
23
+ };
24
+ }
25
+ }
@@ -0,0 +1,2 @@
1
+ import { Finding, Profile, ToolDescriptor } from '../mcp/types.js';
2
+ export declare function runSchemaLints(tools: ToolDescriptor[], profile: Profile): Finding[];
@@ -0,0 +1,65 @@
1
+ function asObject(input) {
2
+ return typeof input === 'object' && input !== null ? input : {};
3
+ }
4
+ export function runSchemaLints(tools, profile) {
5
+ const findings = [];
6
+ for (const tool of tools) {
7
+ const schema = asObject(tool.inputSchema);
8
+ const properties = asObject(schema.properties);
9
+ if (!tool.description && profile !== 'default') {
10
+ findings.push({
11
+ severity: 'low',
12
+ ruleId: 'schema.missing_description',
13
+ message: `Tool ${tool.name} is missing a description`,
14
+ evidence: tool.name,
15
+ remediation: 'Add a concise description for tooling and reviewers.',
16
+ toolName: tool.name
17
+ });
18
+ }
19
+ if (Object.keys(properties).length === 0) {
20
+ findings.push({
21
+ severity: 'medium',
22
+ ruleId: 'schema.empty_properties',
23
+ message: `Tool ${tool.name} does not declare input properties`,
24
+ evidence: JSON.stringify(tool.inputSchema),
25
+ remediation: 'Add explicit JSON Schema properties and constraints.',
26
+ toolName: tool.name
27
+ });
28
+ }
29
+ for (const [name, raw] of Object.entries(properties)) {
30
+ const prop = asObject(raw);
31
+ if (prop.type === 'string' && !('maxLength' in prop) && !('enum' in prop) && !('pattern' in prop)) {
32
+ const severity = profile === 'strict' || profile === 'paranoid' ? 'medium' : 'low';
33
+ findings.push({
34
+ severity,
35
+ ruleId: 'schema.unbounded_string',
36
+ message: `Parameter ${name} on tool ${tool.name} is an unconstrained string`,
37
+ evidence: JSON.stringify(prop),
38
+ remediation: 'Add maxLength, enum, or pattern constraints.',
39
+ toolName: tool.name
40
+ });
41
+ }
42
+ if (prop.type === 'array' && !('maxItems' in prop)) {
43
+ findings.push({
44
+ severity: 'medium',
45
+ ruleId: 'schema.unbounded_array',
46
+ message: `Parameter ${name} on tool ${tool.name} is an unbounded array`,
47
+ evidence: JSON.stringify(prop),
48
+ remediation: 'Add maxItems and constrain item values.',
49
+ toolName: tool.name
50
+ });
51
+ }
52
+ if ((profile === 'strict' || profile === 'paranoid') && /(mode|type|kind|format)/i.test(name) && !('enum' in prop)) {
53
+ findings.push({
54
+ severity: 'low',
55
+ ruleId: 'schema.categorical_missing_enum',
56
+ message: `Categorical field ${name} on tool ${tool.name} should use enum`,
57
+ evidence: JSON.stringify(prop),
58
+ remediation: 'Declare explicit enum values for categorical fields.',
59
+ toolName: tool.name
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return findings;
65
+ }
@@ -0,0 +1 @@
1
+ This is a placeholder. Record a terminal demo (asciinema or GIF) and replace this file.
package/docs/cli.md ADDED
@@ -0,0 +1,38 @@
1
+ # CLI Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm i -g @baichen_yu/mcp-guard
7
+ # or
8
+ npx @baichen_yu/mcp-guard --help
9
+ ```
10
+
11
+ Installed command remains:
12
+
13
+ ```bash
14
+ mcp-guard --help
15
+ ```
16
+
17
+ ## Core commands
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]`
22
+
23
+ ## Scan and registry commands
24
+
25
+ - `mcp-guard scan [--repo <path>] [--path <file>] [--format md|json|sarif] [--out reports]`
26
+ - `mcp-guard registry lint <file>`
27
+ - `mcp-guard registry score <file>`
28
+ - `mcp-guard registry verify <file> --sample 5`
29
+
30
+ ## Exit codes
31
+
32
+ - `0`: clean run (or below policy threshold)
33
+ - `1`: findings exist, but not policy-failing
34
+ - `2`: contract failure, policy failure, or invalid command/config
35
+
36
+ ## Remote transport support
37
+
38
+ `--http` supports HTTP JSON-RPC (POST) only. SSE is not supported.
@@ -0,0 +1,23 @@
1
+ # GitHub Action
2
+
3
+ Use the local composite action to run audits and upload SARIF.
4
+
5
+ ```yaml
6
+ jobs:
7
+ audit:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-node@v4
12
+ with:
13
+ node-version: 20
14
+ - uses: ./.github/actions/mcp-guard
15
+ with:
16
+ stdio_command: node fixtures/servers/hello-mcp-server/server.cjs
17
+ out_dir: reports
18
+ sarif_path: reports/report.sarif
19
+ fail_on: high
20
+ timeout_ms: 30000
21
+ ```
22
+
23
+ SARIF is always uploaded before policy enforcement runs.
package/docs/index.md ADDED
@@ -0,0 +1,75 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: "mcp-guard"
6
+ text: "Practical security gating for MCP servers"
7
+ tagline: "Deterministic MCP validation, contract tests, and audit reporting for local workflows and CI."
8
+ image:
9
+ src: /brand-mark.svg
10
+ alt: mcp-guard
11
+ actions:
12
+ - theme: brand
13
+ text: 30-second quickstart
14
+ link: /quickstart
15
+ - theme: alt
16
+ text: CLI reference
17
+ link: /cli
18
+ - theme: alt
19
+ text: GitHub Action
20
+ link: /github-action
21
+
22
+ features:
23
+ - icon: 🧪
24
+ title: Deterministic contracts
25
+ details: Fixed checks for tools/list, tool call behavior, invalid shapes, cancellation behavior, large payload handling, and timeouts.
26
+ - icon: 🛡️
27
+ title: Policy gate built-in
28
+ details: Use profiles and --fail-on thresholds to enforce guardrails in CI without custom glue code.
29
+ - icon: 📄
30
+ title: Reproducible outputs
31
+ details: Markdown + JSON + SARIF reports designed for review, snapshots, and code-scanning pipelines.
32
+ - icon: 🚦
33
+ title: Two transport modes
34
+ details: Local stdio and remote HTTP JSON-RPC support with bounded retries and timeouts.
35
+ ---
36
+
37
+ > [!IMPORTANT]
38
+ > Remote mode currently supports **HTTP JSON-RPC only** (`--http`). SSE is not implemented.
39
+
40
+ <div class="stats-grid">
41
+ <div class="stat-card"><h4>Profiles</h4><p><code>default</code> · <code>strict</code> · <code>paranoid</code></p></div>
42
+ <div class="stat-card"><h4>Outputs</h4><p><code>report.md</code> · <code>report.json</code> · <code>report.sarif</code></p></div>
43
+ <div class="stat-card"><h4>Policy</h4><p><code>--fail-on off|low|medium|high</code></p></div>
44
+ </div>
45
+
46
+ ## Report preview
47
+
48
+ <div class="report-preview">
49
+ <strong>MCP Guard Report</strong><br/>
50
+ Risk score: <code>100/100</code><br/>
51
+ Key findings: <code>0</code><br/>
52
+ Contract tests: <code>6/6</code><br/>
53
+ Target: <code>node fixtures/servers/hello-mcp-server/server.cjs (stdio)</code>
54
+ </div>
55
+
56
+ ## Architecture
57
+
58
+ ```mermaid
59
+ graph LR
60
+ CLI[mcp-guard CLI] --> T[Transports: stdio/http]
61
+ T --> RPC[JSON-RPC]
62
+ RPC --> RULES[Rules + Profiles]
63
+ RULES --> REP[Reports: md/json/sarif]
64
+ REP --> GATE[Policy Gate (--fail-on)]
65
+ GATE --> CI[CI / Code Scanning]
66
+ ```
67
+
68
+ ## Ship checklist
69
+
70
+ 1. Run `mcp-guard audit` in CI with `--fail-on` policy.
71
+ 2. Upload SARIF so findings show in security dashboards.
72
+ 3. Gate merges on reproducible report output.
73
+
74
+ - GitHub: https://github.com/TomAs-1226/MCP-shariff
75
+ - npm: https://www.npmjs.com/package/@baichen_yu/mcp-guard
@@ -0,0 +1,34 @@
1
+ # Quickstart
2
+
3
+ ## No install (npx)
4
+
5
+ ```bash
6
+ npx @baichen_yu/mcp-guard audit --stdio "node fixtures/servers/hello-mcp-server/server.cjs" --out reports --fail-on off
7
+ ```
8
+
9
+ ## Global install
10
+
11
+ ```bash
12
+ npm i -g @baichen_yu/mcp-guard
13
+ mcp-guard --help
14
+ ```
15
+
16
+ ## Package note
17
+
18
+ The npm package is scoped as `@baichen_yu/mcp-guard` to avoid name collisions, while the runtime CLI command stays `mcp-guard`.
19
+
20
+ For first publish of the scoped package, use:
21
+
22
+ ```bash
23
+ npm publish --access public
24
+ ```
25
+
26
+ ## GitHub Pages docs
27
+
28
+ 1. Enable **Settings → Pages → Source → GitHub Actions** once.
29
+ 2. Docs deploy from `.github/workflows/deploy-pages.yml`.
30
+ 3. Expected URL: `https://<owner>.github.io/MCP-shariff/`.
31
+
32
+ ## Remote mode note
33
+
34
+ Remote mode supports **HTTP JSON-RPC only** (`--http`). SSE is not supported yet.
@@ -0,0 +1,39 @@
1
+ # Releasing
2
+
3
+ ## Prerequisites
4
+
5
+ - npm account has access to `@baichen_yu` scope
6
+ - GitHub Pages source is set to **GitHub Actions**
7
+
8
+ ## First publish (scoped package)
9
+
10
+ ```bash
11
+ npm login
12
+ npm run fixtures:gen
13
+ npm run lint
14
+ npm test
15
+ npm run build
16
+ npm run docs:build
17
+ npm pack --dry-run
18
+ npm publish --access public
19
+ ```
20
+
21
+ Why `--access public`?
22
+ Scoped packages default to private on first publish. Explicitly setting public avoids that surprise.
23
+
24
+ ## Publishing troubleshooting
25
+
26
+ - Run `npm publish` from the project root (the directory that contains `package.json`).
27
+ - If you see `ENOENT` about missing `package.json`, you are in the wrong directory.
28
+
29
+ ## Tag + GitHub Release
30
+
31
+ ```bash
32
+ git tag v0.3.0
33
+ git push origin v0.3.0
34
+ ```
35
+
36
+ Then create a GitHub Release and include:
37
+ - key highlights
38
+ - migration note (formerly mcp-doctor)
39
+ - limitation note (HTTP JSON-RPC only, SSE not supported)
package/docs/rules.md ADDED
@@ -0,0 +1,18 @@
1
+ # Rules and Profiles
2
+
3
+ ## Rules
4
+
5
+ - `schema.empty_properties`: tool schema has no declared properties.
6
+ - `schema.unbounded_string`: string input without maxLength/enum/pattern.
7
+ - `schema.unbounded_array`: array input without maxItems.
8
+ - `schema.missing_description` (strict/paranoid): tool description missing.
9
+ - `schema.categorical_missing_enum` (strict/paranoid): categorical field lacks enum.
10
+ - `security.path_traversal`: path/file parameters without allowlist constraints.
11
+ - `security.shell_injection`: raw command/shell arguments accepted as strings.
12
+ - `security.raw_args`: unbounded argv/flags arrays.
13
+
14
+ ## Profiles
15
+
16
+ - `default`: baseline rule set.
17
+ - `strict`: enables additional schema hygiene checks and heavier scoring.
18
+ - `paranoid`: escalates shell/argv findings to high severity and uses strongest score penalties.
@@ -0,0 +1,14 @@
1
+ # Security Model
2
+
3
+ `mcp-guard` is designed for deterministic, bounded checks.
4
+
5
+ - Contract tests are fixed and do not execute arbitrary tool payloads.
6
+ - Every request has a timeout; transports enforce bounded retries.
7
+ - Child processes are terminated on shutdown.
8
+ - Config scan redacts token-like values in output.
9
+ - Registry verify is offline-only and never executes remote servers.
10
+
11
+ ## Known limitations
12
+
13
+ - HTTP mode currently supports JSON-RPC over POST, not SSE.
14
+ - `scan` reports discovered configurations and metadata, but does not execute every discovered server by default.
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@baichen_yu/mcp-guard",
3
+ "version": "0.3.0",
4
+ "description": "Security auditing and policy gating for MCP servers (STDIO/HTTP) with Markdown + SARIF reports",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-guard": "dist/cli.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc -p tsconfig.json",
14
+ "dev": "tsx src/cli.ts",
15
+ "test": "vitest run",
16
+ "lint": "tsc -p tsconfig.json --noEmit",
17
+ "fixtures:gen": "tsx scripts/gen-fixtures.ts",
18
+ "docs:dev": "vitepress dev docs",
19
+ "docs:build": "vitepress build docs",
20
+ "docs:preview": "vitepress preview docs"
21
+ },
22
+ "dependencies": {
23
+ "commander": "^12.1.0",
24
+ "js-yaml": "^4.1.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/js-yaml": "^4.0.9",
28
+ "@types/node": "^22.10.2",
29
+ "tsx": "^4.19.2",
30
+ "typescript": "^5.7.2",
31
+ "vitest": "^2.1.8",
32
+ "vitepress": "^1.6.3"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE",
38
+ "RELEASE.md",
39
+ "docs/*.md",
40
+ "docs/assets/demo.gif"
41
+ ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/TomAs-1226/MCP-shariff.git"
45
+ },
46
+ "homepage": "https://tomas-1226.github.io/MCP-shariff/",
47
+ "bugs": {
48
+ "url": "https://github.com/TomAs-1226/MCP-shariff/issues"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "main": "dist/cli.js",
54
+ "types": "dist/cli.d.ts",
55
+ "keywords": [
56
+ "mcp",
57
+ "model-context-protocol",
58
+ "security",
59
+ "audit",
60
+ "sarif",
61
+ "cli",
62
+ "lint"
63
+ ]
64
+ }