@permission-protocol/mcp-guard 0.1.0 → 0.2.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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +101 -59
- package/SECURITY.md +40 -0
- package/dist/src/approval-server.d.ts +69 -3
- package/dist/src/approval-server.js +345 -86
- package/dist/src/approval-server.js.map +1 -1
- package/dist/src/approval-ui.d.ts +5 -0
- package/dist/src/approval-ui.js +120 -81
- package/dist/src/approval-ui.js.map +1 -1
- package/dist/src/classify.d.ts +29 -0
- package/dist/src/classify.js +107 -0
- package/dist/src/classify.js.map +1 -0
- package/dist/src/cli.js +49 -12
- package/dist/src/cli.js.map +1 -1
- package/dist/src/config.d.ts +21 -1
- package/dist/src/config.js +92 -5
- package/dist/src/config.js.map +1 -1
- package/dist/src/enforcement/index.d.ts +12 -0
- package/dist/src/enforcement/index.js +53 -0
- package/dist/src/enforcement/index.js.map +1 -0
- package/dist/src/enforcement/policy.d.ts +29 -0
- package/dist/src/enforcement/policy.js +60 -0
- package/dist/src/enforcement/policy.js.map +1 -0
- package/dist/src/enforcement/types.d.ts +19 -0
- package/dist/src/enforcement/types.js +3 -0
- package/dist/src/enforcement/types.js.map +1 -0
- package/dist/src/enforcement/verifier.d.ts +4 -0
- package/dist/src/enforcement/verifier.js +110 -0
- package/dist/src/enforcement/verifier.js.map +1 -0
- package/dist/src/engine.d.ts +11 -2
- package/dist/src/engine.js +20 -14
- package/dist/src/engine.js.map +1 -1
- package/dist/src/frame.d.ts +67 -0
- package/dist/src/frame.js +113 -0
- package/dist/src/frame.js.map +1 -0
- package/dist/src/pending.d.ts +108 -12
- package/dist/src/pending.js +174 -24
- package/dist/src/pending.js.map +1 -1
- package/dist/src/proxy.d.ts +29 -2
- package/dist/src/proxy.js +274 -105
- package/dist/src/proxy.js.map +1 -1
- package/dist/src/receipt-verify.d.ts +36 -0
- package/dist/src/receipt-verify.js +88 -0
- package/dist/src/receipt-verify.js.map +1 -0
- package/dist/src/receipt.d.ts +81 -7
- package/dist/src/receipt.js +100 -17
- package/dist/src/receipt.js.map +1 -1
- package/dist/src/signer.d.ts +38 -0
- package/dist/src/signer.js +112 -0
- package/dist/src/signer.js.map +1 -0
- package/dist/src/verify-cli.d.ts +1 -0
- package/dist/src/verify-cli.js +133 -0
- package/dist/src/verify-cli.js.map +1 -0
- package/dist/src/verify-core.d.ts +22 -0
- package/dist/src/verify-core.js +50 -0
- package/dist/src/verify-core.js.map +1 -0
- package/dist/src/webhook.d.ts +35 -0
- package/dist/src/webhook.js +57 -0
- package/dist/src/webhook.js.map +1 -0
- package/example/test.sh +5 -7
- package/package.json +33 -7
- package/templates/github-actions/receipt-gate.yml +64 -0
- package/assets/demo.gif +0 -0
- package/demo/_demo-scenario.sh +0 -43
- package/dist/src/tests/config.test.d.ts +0 -1
- package/dist/src/tests/config.test.js +0 -63
- package/dist/src/tests/config.test.js.map +0 -1
- package/dist/src/tests/engine.test.d.ts +0 -1
- package/dist/src/tests/engine.test.js +0 -46
- package/dist/src/tests/engine.test.js.map +0 -1
- package/dist/src/tests/receipt.test.d.ts +0 -1
- package/dist/src/tests/receipt.test.js +0 -44
- package/dist/src/tests/receipt.test.js.map +0 -1
- package/docs/index.html +0 -771
- package/permission-protocol-mcp-guard-0.1.0.tgz +0 -0
- package/src/approval-server.ts +0 -136
- package/src/approval-ui.ts +0 -154
- package/src/cli.ts +0 -84
- package/src/config.ts +0 -68
- package/src/engine.ts +0 -31
- package/src/pending.ts +0 -65
- package/src/proxy.ts +0 -173
- package/src/receipt.ts +0 -52
- package/src/tests/config.test.ts +0 -61
- package/src/tests/engine.test.ts +0 -47
- package/src/tests/receipt.test.ts +0 -44
- package/tsconfig.json +0 -16
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — security hardening (breaking)
|
|
4
|
+
|
|
5
|
+
Fixes the findings of the October 2026 MCP Guard audit.
|
|
6
|
+
|
|
7
|
+
### Breaking changes and migration
|
|
8
|
+
|
|
9
|
+
- **Strict config.** Unknown keys, duplicate rules, `countdown_seconds` on a rule that is not `reversibility: reversible`, and `cap_usd` without `amount_arg` now fail at startup. Remove any `tools:` block (it was never read) and add `amount_arg` to capped rules:
|
|
10
|
+
```yaml
|
|
11
|
+
- { id: spend, tool: spend, action: require_approval, cap_usd: 50, amount_arg: amount_usd }
|
|
12
|
+
```
|
|
13
|
+
- **Spend caps read only `amount_arg`.** `_pp.amount_usd` and the `amount_usd`/`amount` fallbacks are gone. A missing or non-numeric amount is a hard stop.
|
|
14
|
+
- **All methods are gated.** `resources/read`, `prompts/get`, `resources/subscribe` and unknown methods now follow the rules and `default_action` (a `default_action: block` policy blocks them unless a `method:` rule allows them). Plumbing still passes through.
|
|
15
|
+
- **Malformed input is refused.** Unparseable lines get `-32700`; JSON-RPC batches and malformed frames get `-32600`. Nothing is forwarded raw.
|
|
16
|
+
- **Approval UI needs its token.** It listens on 127.0.0.1 only; open the URL printed at startup (`…/#t=<token>`). API clients send `Authorization: Bearer <token>`; `PP_APPROVAL_TOKEN` (32+ characters) supplies it. Cross-origin access (`?api=` on another origin) no longer works.
|
|
17
|
+
- **Post-release undo is removed** (`/api/undo` returns 404). Use Hold before a countdown releases. Verify-lane items can only be acknowledged.
|
|
18
|
+
- **Receipt v2.** New signed envelope; v1 receipts are rejected by the verifier. Receipts carry `args_sha256` and argument names, never raw arguments.
|
|
19
|
+
- **Keys moved** from `./.keys` to `~/.config/mcp-guard/keys` (or `$PP_KEYS_DIR`). Key ids are now `pp-dev-<fingerprint>`. Re-export and re-pin the public key: `mcp-guard export-key`.
|
|
20
|
+
- **`verify` requires `--pubkey` and `--scope`.** The verifier no longer generates or discovers a key, and empty `--scope-*` values are errors.
|
|
21
|
+
- **GitHub commit-status poster removed** (`PP_GITHUB_TOKEN`). Gate merges with the Permission Protocol GitHub App's `permission-protocol/approval` check.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `mcp-guard verify` and `mcp-guard export-key` subcommands.
|
|
26
|
+
- `--operator <name>`: approvals record `{kind: human, id, via: local-token}`; countdown and standing-rule releases record `{kind: policy}`.
|
|
27
|
+
- `approval_ttl_seconds` and `receipt_ttl_seconds` config keys (default 900 each). Expired held requests answer the agent with `-32004`; denials with `-32003`.
|
|
28
|
+
- The approval UI and the Permission Deck show every argument the server will receive (`args_full`) and the scope an approval signs for (`scope`).
|
|
29
|
+
- The wrapped server no longer inherits `PP_*` environment variables (approval token, webhook secret, receipt token).
|
|
30
|
+
- `merge_pr` scope ref is derived only from the PR number; `_pp.requires_input` no longer moves an irreversible hold out of the Decide lane.
|
|
31
|
+
- GitHub webhook deduplicates `X-GitHub-Delivery`.
|
|
32
|
+
- `templates/github-actions/receipt-gate.yml`: a deploy gate that takes its verifier, key and receipt only from trusted sources.
|
|
33
|
+
- CI workflow running build, tests and the end-to-end demo drivers.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Approval API: no authentication, all interfaces, `Access-Control-Allow-Origin: *`.
|
|
38
|
+
- Spend-cap bypass through `_pp.amount_usd`.
|
|
39
|
+
- Receipt signatures that covered only part of the receipt and could be re-split across fields.
|
|
40
|
+
- Self-gate CI that trusted the key, receipt and verifier from the pull request (workflow removed).
|
|
41
|
+
- Fail-open forwarding of unparseable lines and batches.
|
|
42
|
+
- Approving a webhook PR after it was denied (and approving repeatedly).
|
|
43
|
+
- Countdown releases producing receipts indistinguishable from human approval.
|
|
44
|
+
- Raw tool arguments in stderr, receipts and the CORS-open receipts API.
|
|
45
|
+
- Enforcement module: a missing or invalid policy meant "nothing protected"; duplicate signer names counted as separate approvals.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Permission Protocol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
[MCP](https://modelcontextprotocol.io) (Model Context Protocol) is how AI agents call tools — like APIs, databases, or deployments. MCP has no built-in access control. MCP Guard adds that layer.
|
|
6
6
|
|
|
7
|
-
Define what's allowed, blocked, or requires approval — in a single YAML file. MCP Guard sits between your MCP client and server, enforces the rules, and
|
|
7
|
+
Define what's allowed, blocked, or requires approval — in a single YAML file. MCP Guard sits between your MCP client and server, enforces the rules, and records every decision as a signed receipt.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Every tool call, resource read and prompt fetch goes through your policy; only protocol plumbing (`initialize`, `ping`, list calls, notifications) passes untouched. **MCP Guard does not just log — it blocks execution before it happens.**
|
|
10
10
|
|
|
11
11
|
→ **Blocks** unsafe tool calls before they execute
|
|
12
12
|
→ **Holds** sensitive actions for human approval
|
|
13
|
-
→ **
|
|
13
|
+
→ **Signs** every decision as an Ed25519 receipt that names who authorized it
|
|
14
14
|
→ **Observe mode** — audit what would be blocked before enforcing
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
@@ -21,7 +21,7 @@ Without MCP Guard, your agent can call any tool. With it, every action is checke
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
# Install
|
|
24
|
-
npm install @
|
|
24
|
+
npm install @permission-protocol/mcp-guard
|
|
25
25
|
|
|
26
26
|
# Create a policy file
|
|
27
27
|
cat > pp.config.yaml << 'EOF'
|
|
@@ -39,19 +39,18 @@ EOF
|
|
|
39
39
|
mcp-guard --config pp.config.yaml -- node my-mcp-server.js
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
That's it. Your agent can no longer delete user data. Production deploys require approval.
|
|
42
|
+
That's it. Your agent can no longer delete user data. Production deploys require approval. Every decision is signed and logged to `pp-receipts.jsonl`.
|
|
43
|
+
|
|
44
|
+
`default_action: allow` lets any tool without a rule run, and the guard warns about it at startup. Use `block` or `require_approval` to fail closed.
|
|
43
45
|
|
|
44
46
|
## 60-Second Demo (no MCP server needed)
|
|
45
47
|
|
|
48
|
+
See MCP Guard block dangerous tool calls in real-time, no setup required:
|
|
49
|
+
|
|
46
50
|
```bash
|
|
47
|
-
|
|
48
|
-
cd mcp-guard
|
|
49
|
-
npm install
|
|
50
|
-
npm run demo
|
|
51
|
+
npx @permission-protocol/mcp-guard demo
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
Sends three tool calls through the guard and shows: one allowed, one blocked, one held. No setup required.
|
|
54
|
-
|
|
55
54
|
## Architecture
|
|
56
55
|
|
|
57
56
|
```
|
|
@@ -66,29 +65,47 @@ Sends three tool calls through the guard and shows: one allowed, one blocked, on
|
|
|
66
65
|
(audit trail)
|
|
67
66
|
```
|
|
68
67
|
|
|
69
|
-
MCP Guard
|
|
68
|
+
MCP Guard parses every JSON-RPC message on stdin and forwards only what it parsed and evaluated, re-serialized:
|
|
70
69
|
|
|
71
|
-
1.
|
|
72
|
-
2.
|
|
73
|
-
3.
|
|
74
|
-
4.
|
|
75
|
-
5.
|
|
76
|
-
|
|
77
|
-
All other JSON-RPC methods pass through transparently.
|
|
70
|
+
1. A line that is not a single JSON-RPC 2.0 object (invalid JSON, a batch array, a malformed frame) gets an error and is never forwarded.
|
|
71
|
+
2. Plumbing passes through: `initialize`, `ping`, `notifications/*`, `tools/list`, `resources/list`, `resources/templates/list`, `resources/unsubscribe`, `prompts/list`, `logging/setLevel`, `completion/complete`.
|
|
72
|
+
3. Everything else — `tools/call`, `resources/read`, `prompts/get`, `resources/subscribe` and any method MCP Guard does not know — is decided by the rules, then `default_action`.
|
|
73
|
+
4. **Allowed** → forwarded (agent metadata under `_pp` is stripped first). **Blocked** → error `-32001`. **Held** → waits for a human (or returns `-32002` without an approval UI).
|
|
74
|
+
5. Every decision emits a signed receipt (one stderr line + `pp-receipts.jsonl`).
|
|
78
75
|
|
|
79
76
|
## Config Reference
|
|
80
77
|
|
|
81
78
|
```yaml
|
|
82
79
|
# pp.config.yaml
|
|
83
|
-
default_action:
|
|
84
|
-
mode: enforce
|
|
80
|
+
default_action: block # allow | block | require_approval — when no rule matches
|
|
81
|
+
mode: enforce # "enforce" (default) or "observe" (log only, never block)
|
|
82
|
+
approval_ttl_seconds: 900 # held requests expire after this (default 900)
|
|
83
|
+
receipt_ttl_seconds: 900 # signed receipts expire after this (default 900)
|
|
85
84
|
|
|
86
85
|
rules:
|
|
87
|
-
- id: unique-rule-id
|
|
88
|
-
tool: tool_name
|
|
89
|
-
action: allow
|
|
86
|
+
- id: unique-rule-id # unique identifier
|
|
87
|
+
tool: tool_name # exact tool name (tools/call rules)
|
|
88
|
+
action: allow # allow | block | require_approval
|
|
89
|
+
|
|
90
|
+
- id: hold-reads
|
|
91
|
+
method: resources/read # govern another method (no "tool" field)
|
|
92
|
+
action: require_approval
|
|
93
|
+
|
|
94
|
+
- id: hold-email
|
|
95
|
+
tool: send_email
|
|
96
|
+
action: require_approval
|
|
97
|
+
reversibility: reversible # reversible | irreversible (default: irreversible)
|
|
98
|
+
countdown_seconds: 300 # reversible only: auto-release unless someone holds it
|
|
99
|
+
|
|
100
|
+
- id: spend
|
|
101
|
+
tool: spend
|
|
102
|
+
action: require_approval
|
|
103
|
+
cap_usd: 50 # under the cap: runs now, shows in Verify; at/over: hard stop
|
|
104
|
+
amount_arg: amount_usd # required with cap_usd: the argument the tool actually charges
|
|
90
105
|
```
|
|
91
106
|
|
|
107
|
+
The loader is strict: an unknown key, a duplicate rule, a countdown on an irreversible rule, or `cap_usd` without `amount_arg` fails at startup instead of being ignored. A missing or non-numeric amount is a hard stop. Agent metadata under `_pp` (`confidence`, `requires_input`) can route a card to a different lane but can never turn a held or blocked call into an allowed one, and `requires_input` never changes how an irreversible hold is presented.
|
|
108
|
+
|
|
92
109
|
### Modes
|
|
93
110
|
|
|
94
111
|
| Mode | Behavior |
|
|
@@ -104,45 +121,72 @@ Use `observe` mode to audit what *would* be blocked before turning enforcement o
|
|
|
104
121
|
|--------|----------|-------------------|
|
|
105
122
|
| `allow` | Forward request to server | — |
|
|
106
123
|
| `block` | Reject immediately | `-32001` |
|
|
107
|
-
| `require_approval` |
|
|
124
|
+
| `require_approval` | Hold for approval (or reject without an approval UI) | `-32002` |
|
|
125
|
+
|
|
126
|
+
Other errors the agent can receive: `-32003` denied by a human, `-32004` approval request expired, `-32005` release refused (receipt failed verification), `-32700` / `-32600` / `-32602` malformed input.
|
|
108
127
|
|
|
109
128
|
## Receipts
|
|
110
129
|
|
|
111
|
-
Every
|
|
130
|
+
Every decision produces a signed receipt (v2): a small **envelope** that is signed, plus a **display** block for people that is not.
|
|
112
131
|
|
|
113
132
|
```json
|
|
114
133
|
{
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
"envelope": {
|
|
135
|
+
"v": 2,
|
|
136
|
+
"receipt_id": "rcpt_mg_mvtj9td5_63c091bfe0e7",
|
|
137
|
+
"method": "tools/call",
|
|
138
|
+
"tool": "deploy",
|
|
139
|
+
"args_sha256": "<sha256 of the canonical arguments the server received>",
|
|
140
|
+
"scope": { "scope": "deploy:production", "ref": null, "sha": "a1b2c3d4" },
|
|
141
|
+
"decision": "allowed",
|
|
142
|
+
"approval": { "kind": "human", "id": "rod", "via": "local-token" },
|
|
143
|
+
"actor": "my-agent",
|
|
144
|
+
"rule_id": "deploy",
|
|
145
|
+
"policy_sha256": "<sha256 of pp.config.yaml>",
|
|
146
|
+
"mode": "enforce",
|
|
147
|
+
"issued_at": "2026-10-29T15:30:00.000Z",
|
|
148
|
+
"expires_at": "2026-10-29T15:45:00.000Z",
|
|
149
|
+
"nonce": "<random>",
|
|
150
|
+
"key_id": "pp-dev-3c1d2e4f5a6b7c8d"
|
|
151
|
+
},
|
|
152
|
+
"signature": { "alg": "ed25519", "key_id": "pp-dev-3c1d2e4f5a6b7c8d", "value": "<hex>" },
|
|
153
|
+
"display": { "summary": "…", "reason": "…", "risk_tier": "high", "target_server": "node server.js", "arg_keys": ["env", "sha"], "viewer_url": "…" }
|
|
125
154
|
}
|
|
126
155
|
```
|
|
127
156
|
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
157
|
+
- The signature covers `pp-mcp-guard-receipt-v2\n` + canonical JSON of the whole envelope, so no authority field can be edited or re-split.
|
|
158
|
+
- `approval` says who authorized the action: `human` (the approval UI operator) or `policy` (a rule, a standing spend cap, or a countdown).
|
|
159
|
+
- Receipts never contain raw arguments — only `args_sha256` and the argument names, plus the scope identifiers a scoped receipt is bound to (environment, ref, commit SHA). Raw arguments appear only in the authenticated approval UI, which shows every argument and the scope an approval signs for.
|
|
160
|
+
- The signing key lives in `~/.config/mcp-guard/keys` (or `$PP_KEYS_DIR`), never in the working directory. Its id is derived from the public key. MCP Guard refuses to start if the private key is readable by others.
|
|
131
161
|
|
|
132
|
-
|
|
162
|
+
Receipts are written to stderr (one summary line), `pp-receipts.jsonl` (or `$PP_SHARED_RECEIPTS_PATH`), and optionally POSTed to `$PP_RECEIPT_ENDPOINT` (with `$PP_RECEIPT_TOKEN` as bearer auth).
|
|
133
163
|
|
|
134
|
-
|
|
164
|
+
### Verifying a receipt
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Once: export the public key and commit it on your default branch
|
|
168
|
+
mcp-guard export-key .pp/mcp-guard.pub.pem
|
|
169
|
+
|
|
170
|
+
# In CI: verify offline against the pinned key
|
|
171
|
+
mcp-guard verify --receipt-file receipt.json --pubkey .pp/mcp-guard.pub.pem \
|
|
172
|
+
--scope deploy:production --scope-sha "$GITHUB_SHA"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The verifier requires `--scope`, accepts only the keys you pin, requires an unexpired `allowed` receipt, checks scope exactly, and refuses scoped receipts that no human approved (pass `--allow-policy-approval` to accept them). Exit 0 = authorized, 1 = refused, 2 = missing input. See `templates/github-actions/receipt-gate.yml` for a deploy workflow that takes the verifier, key and receipt only from trusted places.
|
|
135
176
|
|
|
136
177
|
## CLI
|
|
137
178
|
|
|
138
179
|
```
|
|
139
180
|
mcp-guard [options] -- <server command>
|
|
181
|
+
mcp-guard verify --receipt-file <path> --pubkey <pem> [--scope s] [--scope-ref r] [--scope-sha h]
|
|
182
|
+
mcp-guard export-key [out.pem]
|
|
140
183
|
|
|
141
184
|
Options:
|
|
142
|
-
--config <path>
|
|
143
|
-
--agent-id <id>
|
|
185
|
+
--config <path> Path to config file (default: ./pp.config.yaml)
|
|
186
|
+
--agent-id <id> Agent identifier for receipts (default: "unknown")
|
|
187
|
+
--operator <name> Approver name recorded on receipts (default: OS username)
|
|
144
188
|
--mode <mode> enforce or observe (overrides config file)
|
|
145
|
-
--approval-port <port> Enable approval UI on
|
|
189
|
+
--approval-port <port> Enable the approval UI on 127.0.0.1:<port>
|
|
146
190
|
-h, --help Show help
|
|
147
191
|
```
|
|
148
192
|
|
|
@@ -174,29 +218,27 @@ With:
|
|
|
174
218
|
}
|
|
175
219
|
```
|
|
176
220
|
|
|
177
|
-
## Approval Flow
|
|
221
|
+
## Approval Flow
|
|
178
222
|
|
|
179
|
-
When a
|
|
223
|
+
When a call matches a `require_approval` rule, MCP Guard can hold it open and wait for a human in a local web UI instead of rejecting it.
|
|
180
224
|
|
|
181
225
|
```bash
|
|
182
|
-
# Start with approval UI on port 3100
|
|
183
226
|
mcp-guard --config pp.config.yaml --approval-port 3100 -- node my-mcp-server.js
|
|
184
|
-
|
|
185
|
-
# Open http://localhost:3100 to see pending approvals
|
|
227
|
+
# stderr: [mcp-guard] Approval UI: http://127.0.0.1:3100/#t=<token>
|
|
186
228
|
```
|
|
187
229
|
|
|
230
|
+
Open the exact URL printed at startup. The part after `#t=` is a per-process access token; it never leaves the browser except as the `Authorization: Bearer` header on API calls.
|
|
231
|
+
|
|
188
232
|
**How it works:**
|
|
189
|
-
1. Agent calls a tool that requires approval
|
|
190
|
-
2.
|
|
191
|
-
3.
|
|
192
|
-
4.
|
|
193
|
-
5.
|
|
233
|
+
1. Agent calls a tool that requires approval; MCP Guard holds the response open.
|
|
234
|
+
2. The call appears in the UI with every argument the server will receive.
|
|
235
|
+
3. **Approve** → MCP Guard signs a receipt naming you as the approver, verifies it, then forwards the call.
|
|
236
|
+
4. **Deny** → the agent gets error `-32003`. A decision is final: approving a denied call returns 409.
|
|
237
|
+
5. No decision within `approval_ttl_seconds` → the agent gets error `-32004`.
|
|
194
238
|
|
|
195
|
-
|
|
239
|
+
**Security of the approval channel:** it listens on 127.0.0.1 only, rejects any other `Host` header, sends no CORS headers, and requires the token on every API route except the HMAC-signed GitHub webhook. `PP_APPROVAL_TOKEN` (32+ characters) supplies the token instead of generating one. The wrapped server is started without any `PP_*` environment variables, so a tool that echoes its environment cannot leak the token.
|
|
196
240
|
|
|
197
|
-
|
|
198
|
-
- Pending tool calls with collapsible arguments
|
|
199
|
-
- Recent decision history (last 20)
|
|
241
|
+
When `--approval-port` is **not** set, `require_approval` calls return `-32002` immediately.
|
|
200
242
|
|
|
201
243
|
## Why not just use X?
|
|
202
244
|
|
|
@@ -210,7 +252,7 @@ The approval UI auto-refreshes every 3 seconds and shows:
|
|
|
210
252
|
|
|
211
253
|
- **Not a dashboard.** It's a proxy. It sits in the data path and enforces rules.
|
|
212
254
|
- **Not a scanner.** It doesn't analyze your code or model outputs. It gates tool calls.
|
|
213
|
-
- **Not compliance theater.** Every decision has
|
|
255
|
+
- **Not compliance theater.** Every decision has an Ed25519-signed receipt that names who authorized it.
|
|
214
256
|
- **Not a replacement for good architecture.** It's one layer in a defense-in-depth strategy.
|
|
215
257
|
|
|
216
258
|
Agents shouldn't be able to execute arbitrary actions without an explicit decision point.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
This repository contains MCP Guard, the Permission Protocol stdio policy proxy for MCP tool calls.
|
|
4
|
+
|
|
5
|
+
## Reporting a vulnerability
|
|
6
|
+
|
|
7
|
+
**Do not open public issues for security vulnerabilities.**
|
|
8
|
+
|
|
9
|
+
Email: security@permissionprotocol.com
|
|
10
|
+
|
|
11
|
+
Include a description, steps to reproduce, and your assessment of impact. We acknowledge reports within 48 hours and keep you informed through resolution.
|
|
12
|
+
|
|
13
|
+
Machine-readable contact: https://www.permissionprotocol.com/.well-known/security.txt
|
|
14
|
+
|
|
15
|
+
## Threat model
|
|
16
|
+
|
|
17
|
+
MCP Guard assumes the agent behind the MCP client is untrusted: it controls every tool argument, can send any bytes on stdin, and may be able to reach local ports. MCP Guard defends against that agent:
|
|
18
|
+
|
|
19
|
+
- **Input:** only single, well-formed JSON-RPC 2.0 objects are evaluated, and only the evaluated object is forwarded (re-serialized). Batches and unparseable lines are refused.
|
|
20
|
+
- **Policy:** every method except protocol plumbing is decided by the rules. Agent metadata (`_pp`) can add human attention but never removes it; spend caps read only the argument the rule names.
|
|
21
|
+
- **Approval channel:** loopback only, Host-checked, no CORS, and a per-process bearer token delivered in the URL fragment. Decisions go through one state machine; final states never change.
|
|
22
|
+
- **Receipts:** Ed25519 over a domain-separated canonical envelope, with expiry, nonce and approver. Verifiers trust only pinned keys and refuse scoped receipts no human approved.
|
|
23
|
+
- **Keys:** kept in the operator's config directory with owner-only permissions; MCP Guard refuses to start if the private key is exposed.
|
|
24
|
+
|
|
25
|
+
Out of scope for the local dev signer: an agent running as the **same OS user** with arbitrary shell access can read that user's files and process memory, including the signing key and the approval token. For that threat, run the agent as a different user or use the managed Permission Protocol signer, where approval and signing happen outside the agent's machine.
|
|
26
|
+
|
|
27
|
+
## Safe harbor
|
|
28
|
+
|
|
29
|
+
We will not pursue or support legal action against good-faith security research that:
|
|
30
|
+
|
|
31
|
+
- respects user privacy and does not access, modify, or destroy data that is not yours;
|
|
32
|
+
- avoids service disruption (no denial of service, no spam);
|
|
33
|
+
- gives us reasonable time to remediate before public disclosure.
|
|
34
|
+
|
|
35
|
+
If you are unsure whether something is in scope, ask first at the address above.
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- There is no formal bug bounty program today. With your permission, we credit reporters in release notes.
|
|
40
|
+
- Our security and compliance posture is published at https://www.permissionprotocol.com/trust.
|
|
@@ -1,4 +1,70 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PendingAction } from './pending.js';
|
|
2
|
+
import type { Lane } from './engine.js';
|
|
3
|
+
import type { Reversibility } from './config.js';
|
|
4
|
+
import { type Approval, type Receipt } from './receipt.js';
|
|
5
|
+
/**
|
|
6
|
+
* The local approval channel.
|
|
7
|
+
*
|
|
8
|
+
* - Listens on 127.0.0.1 only and rejects any Host header but its own (DNS rebinding).
|
|
9
|
+
* - Sends no CORS headers: other origins cannot read or drive it.
|
|
10
|
+
* - Every /api route except the GitHub webhook needs `Authorization: Bearer <token>`.
|
|
11
|
+
* The token is generated per process (or supplied via PP_APPROVAL_TOKEN) and reaches
|
|
12
|
+
* the browser only in the URL fragment printed at startup; GET / holds no secret.
|
|
13
|
+
* - Every decision goes through the pending-queue state machine, so final states hold.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MIN_TOKEN_LENGTH = 32;
|
|
2
16
|
export declare function pushReceipt(receipt: Receipt): void;
|
|
3
|
-
export
|
|
4
|
-
|
|
17
|
+
export interface ApprovalHooks {
|
|
18
|
+
/** Release a held request the human approved: sign, forward, return the receipt. */
|
|
19
|
+
release?: (action: PendingAction, approval: Approval) => Receipt;
|
|
20
|
+
}
|
|
21
|
+
export interface ApprovalServerOptions {
|
|
22
|
+
port: number;
|
|
23
|
+
/** Bearer token; defaults to PP_APPROVAL_TOKEN, else 32 random bytes. */
|
|
24
|
+
token?: string;
|
|
25
|
+
/** Recorded as the approver on human decisions; defaults to the OS username. */
|
|
26
|
+
operator?: string;
|
|
27
|
+
hooks?: ApprovalHooks;
|
|
28
|
+
/** For receipts issued on webhook-sourced decisions. */
|
|
29
|
+
receiptTtlSeconds?: number;
|
|
30
|
+
policySha256?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ApprovalServerHandle {
|
|
33
|
+
token: string;
|
|
34
|
+
operator: string;
|
|
35
|
+
/** Resolves with the bound port once listening. */
|
|
36
|
+
ready: Promise<number>;
|
|
37
|
+
close: () => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** The QueueItem shape from the API contract (backend ⇄ console). */
|
|
40
|
+
export interface QueueItem {
|
|
41
|
+
id: string;
|
|
42
|
+
tool_name: string;
|
|
43
|
+
lane: Lane;
|
|
44
|
+
reversibility: Reversibility;
|
|
45
|
+
countdown_seconds?: number;
|
|
46
|
+
countdown_remaining?: number;
|
|
47
|
+
confidence?: number;
|
|
48
|
+
summary: string;
|
|
49
|
+
args_preview: string;
|
|
50
|
+
/** Every argument the server will receive, so the approver sees all of it. */
|
|
51
|
+
args_full: string;
|
|
52
|
+
/** What an approval authorizes, when the receipt is scoped (merge, migration, deploy). */
|
|
53
|
+
scope?: {
|
|
54
|
+
scope: string;
|
|
55
|
+
ref: string | null;
|
|
56
|
+
sha: string | null;
|
|
57
|
+
};
|
|
58
|
+
agent_id: string;
|
|
59
|
+
rule_id: string | null;
|
|
60
|
+
status: PendingAction['status'];
|
|
61
|
+
acted: boolean;
|
|
62
|
+
created_at: string;
|
|
63
|
+
expires_at?: string;
|
|
64
|
+
receipt_id?: string;
|
|
65
|
+
}
|
|
66
|
+
export declare function toQueueItem(p: PendingAction): QueueItem;
|
|
67
|
+
export declare function pendingQueue(): QueueItem[];
|
|
68
|
+
/** Call after any queue mutation so SSE subscribers see it immediately. */
|
|
69
|
+
export declare function notifyQueueChange(): void;
|
|
70
|
+
export declare function startApprovalServer(opts: ApprovalServerOptions): ApprovalServerHandle;
|