@permission-protocol/mcp-guard 0.1.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 (59) hide show
  1. package/README.md +224 -0
  2. package/assets/demo.gif +0 -0
  3. package/bin/mcp-guard +2 -0
  4. package/demo/_demo-scenario.sh +43 -0
  5. package/dist/example/server.d.ts +1 -0
  6. package/dist/example/server.js +54 -0
  7. package/dist/example/server.js.map +1 -0
  8. package/dist/src/approval-server.d.ts +4 -0
  9. package/dist/src/approval-server.js +128 -0
  10. package/dist/src/approval-server.js.map +1 -0
  11. package/dist/src/approval-ui.d.ts +1 -0
  12. package/dist/src/approval-ui.js +158 -0
  13. package/dist/src/approval-ui.js.map +1 -0
  14. package/dist/src/cli.d.ts +1 -0
  15. package/dist/src/cli.js +82 -0
  16. package/dist/src/cli.js.map +1 -0
  17. package/dist/src/config.d.ts +13 -0
  18. package/dist/src/config.js +48 -0
  19. package/dist/src/config.js.map +1 -0
  20. package/dist/src/engine.d.ts +7 -0
  21. package/dist/src/engine.js +25 -0
  22. package/dist/src/engine.js.map +1 -0
  23. package/dist/src/pending.d.ts +20 -0
  24. package/dist/src/pending.js +44 -0
  25. package/dist/src/pending.js.map +1 -0
  26. package/dist/src/proxy.d.ts +2 -0
  27. package/dist/src/proxy.js +147 -0
  28. package/dist/src/proxy.js.map +1 -0
  29. package/dist/src/receipt.d.ts +15 -0
  30. package/dist/src/receipt.js +34 -0
  31. package/dist/src/receipt.js.map +1 -0
  32. package/dist/src/tests/config.test.d.ts +1 -0
  33. package/dist/src/tests/config.test.js +63 -0
  34. package/dist/src/tests/config.test.js.map +1 -0
  35. package/dist/src/tests/engine.test.d.ts +1 -0
  36. package/dist/src/tests/engine.test.js +46 -0
  37. package/dist/src/tests/engine.test.js.map +1 -0
  38. package/dist/src/tests/receipt.test.d.ts +1 -0
  39. package/dist/src/tests/receipt.test.js +44 -0
  40. package/dist/src/tests/receipt.test.js.map +1 -0
  41. package/docs/index.html +771 -0
  42. package/example/README.md +38 -0
  43. package/example/pp.config.yaml +15 -0
  44. package/example/server.ts +52 -0
  45. package/example/test.sh +64 -0
  46. package/package.json +29 -0
  47. package/permission-protocol-mcp-guard-0.1.0.tgz +0 -0
  48. package/src/approval-server.ts +136 -0
  49. package/src/approval-ui.ts +154 -0
  50. package/src/cli.ts +84 -0
  51. package/src/config.ts +68 -0
  52. package/src/engine.ts +31 -0
  53. package/src/pending.ts +65 -0
  54. package/src/proxy.ts +173 -0
  55. package/src/receipt.ts +52 -0
  56. package/src/tests/config.test.ts +61 -0
  57. package/src/tests/engine.test.ts +47 -0
  58. package/src/tests/receipt.test.ts +44 -0
  59. package/tsconfig.json +16 -0
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # MCP Guard
2
+
3
+ **Stop your AI agents from doing dangerous things.**
4
+
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
+
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 logs every decision as an audit receipt.
8
+
9
+ Without MCP Guard, your agent can call any tool. With it, every action is checked. **MCP Guard does not just log — it blocks execution before it happens.**
10
+
11
+ → **Blocks** unsafe tool calls before they execute
12
+ → **Holds** sensitive actions for human approval
13
+ → **Logs** every decision as an immutable receipt
14
+ → **Observe mode** — audit what would be blocked before enforcing
15
+
16
+ <p align="center">
17
+ <img src="assets/demo.gif" alt="MCP Guard Demo" width="700">
18
+ </p>
19
+
20
+ ## Get Started (2 minutes)
21
+
22
+ ```bash
23
+ # Install
24
+ npm install @permissionprotocol/mcp-guard
25
+
26
+ # Create a policy file
27
+ cat > pp.config.yaml << 'EOF'
28
+ default_action: allow
29
+ rules:
30
+ - id: block-delete
31
+ tool: delete_user_data
32
+ action: block
33
+ - id: hold-deploy
34
+ tool: deploy_production
35
+ action: require_approval
36
+ EOF
37
+
38
+ # Run your MCP server through the guard
39
+ mcp-guard --config pp.config.yaml -- node my-mcp-server.js
40
+ ```
41
+
42
+ That's it. Your agent can no longer delete user data. Production deploys require approval. Everything is logged to `pp-receipts.jsonl`.
43
+
44
+ ## 60-Second Demo (no MCP server needed)
45
+
46
+ ```bash
47
+ git clone https://github.com/permission-protocol/mcp-guard
48
+ cd mcp-guard
49
+ npm install
50
+ npm run demo
51
+ ```
52
+
53
+ Sends three tool calls through the guard and shows: one allowed, one blocked, one held. No setup required.
54
+
55
+ ## Architecture
56
+
57
+ ```
58
+ ┌────────────┐ stdio ┌─────────────┐ stdio ┌────────────┐
59
+ │ MCP Client│ ──────────────▶│ MCP Guard │──────────────▶ │ MCP Server │
60
+ │ (Claude, │ │ (proxy) │ │ (your app) │
61
+ │ Cursor) │ ◀──────────────│ │◀────────────── │ │
62
+ └────────────┘ responses └─────────────┘ responses └────────────┘
63
+ │
64
+ ▼
65
+ pp-receipts.jsonl
66
+ (audit trail)
67
+ ```
68
+
69
+ MCP Guard intercepts JSON-RPC messages on stdin/stdout. When it sees a `tools/call` request:
70
+
71
+ 1. Looks up the tool name in the config rules
72
+ 2. If **allowed** → forwards to the real server
73
+ 3. If **blocked** → returns a JSON-RPC error (`-32001`) directly
74
+ 4. If **held for approval** → returns a JSON-RPC error (`-32002`) directly
75
+ 5. Emits a receipt for every decision (stderr + jsonl file)
76
+
77
+ All other JSON-RPC methods pass through transparently.
78
+
79
+ ## Config Reference
80
+
81
+ ```yaml
82
+ # pp.config.yaml
83
+ default_action: allow # "allow" or "block" — applies when no rule matches
84
+ mode: enforce # "enforce" (default) or "observe" (log only, never block)
85
+
86
+ rules:
87
+ - id: unique-rule-id # Human-readable identifier
88
+ tool: tool_name # Exact match on MCP tool name
89
+ action: allow # allow | block | require_approval
90
+ ```
91
+
92
+ ### Modes
93
+
94
+ | Mode | Behavior |
95
+ |------|----------|
96
+ | `enforce` | Block/hold tool calls per rules (default) |
97
+ | `observe` | Log decisions + emit receipts, but always forward (dry-run) |
98
+
99
+ Use `observe` mode to audit what *would* be blocked before turning enforcement on. Switch via config or CLI: `--mode observe`.
100
+
101
+ ### Actions
102
+
103
+ | Action | Behavior | JSON-RPC Error Code |
104
+ |--------|----------|-------------------|
105
+ | `allow` | Forward request to server | — |
106
+ | `block` | Reject immediately | `-32001` |
107
+ | `require_approval` | Reject with hold status | `-32002` |
108
+
109
+ ## Receipts
110
+
111
+ Every `tools/call` decision generates an immutable receipt:
112
+
113
+ ```json
114
+ {
115
+ "receipt_id": "550e8400-e29b-41d4-a716-446655440000",
116
+ "timestamp": "2026-03-20T15:30:00.000Z",
117
+ "agent_id": "my-agent",
118
+ "tool_name": "delete_user_data",
119
+ "decision": "blocked",
120
+ "reason": "Matched rule \"block-dangerous-delete\"",
121
+ "rule_id": "block-dangerous-delete",
122
+ "request_payload_hash": "sha256-hex-string",
123
+ "target_server": "node my-mcp-server.js",
124
+ "mode": "enforce"
125
+ }
126
+ ```
127
+
128
+ Receipts are written to:
129
+ - **stderr** — for real-time monitoring
130
+ - **pp-receipts.jsonl** — append-only audit file
131
+
132
+ The `request_payload_hash` is a SHA-256 of the full request params, so you can verify what was sent without storing sensitive arguments.
133
+
134
+ **[→ Visualize your receipts](https://permission-protocol.github.io/mcp-guard/)** — paste a receipt or upload `pp-receipts.jsonl` to see a visual summary.
135
+
136
+ ## CLI
137
+
138
+ ```
139
+ mcp-guard [options] -- <server command>
140
+
141
+ Options:
142
+ --config <path> Path to config file (default: ./pp.config.yaml)
143
+ --agent-id <id> Agent identifier for receipts (default: "unknown")
144
+ --mode <mode> enforce or observe (overrides config file)
145
+ --approval-port <port> Enable approval UI on this port (e.g. 3100)
146
+ -h, --help Show help
147
+ ```
148
+
149
+ ### Example: Claude Desktop
150
+
151
+ In your Claude Desktop MCP config, replace:
152
+
153
+ ```json
154
+ {
155
+ "mcpServers": {
156
+ "my-server": {
157
+ "command": "node",
158
+ "args": ["my-mcp-server.js"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ With:
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "my-server": {
170
+ "command": "mcp-guard",
171
+ "args": ["--config", "pp.config.yaml", "--agent-id", "claude-desktop", "--", "node", "my-mcp-server.js"]
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ ## Approval Flow (v0.1)
178
+
179
+ When a tool call matches a `require_approval` rule, MCP Guard can hold the request open and wait for human approval via a web UI — instead of rejecting immediately.
180
+
181
+ ```bash
182
+ # Start with approval UI on port 3100
183
+ mcp-guard --config pp.config.yaml --approval-port 3100 -- node my-mcp-server.js
184
+
185
+ # Open http://localhost:3100 to see pending approvals
186
+ ```
187
+
188
+ **How it works:**
189
+ 1. Agent calls a tool that requires approval
190
+ 2. MCP Guard holds the JSON-RPC response open (does NOT return an error)
191
+ 3. The tool call appears in the approval UI at `http://localhost:<port>`
192
+ 4. Human clicks **Approve** → request is forwarded to the server, response returned to the agent
193
+ 5. Human clicks **Deny** → JSON-RPC error (-32002) returned to the agent
194
+
195
+ When `--approval-port` is **not** set, `require_approval` tools return an error immediately (original behavior, no change).
196
+
197
+ The approval UI auto-refreshes every 3 seconds and shows:
198
+ - Pending tool calls with collapsible arguments
199
+ - Recent decision history (last 20)
200
+
201
+ ## Why not just use X?
202
+
203
+ | Approach | Why MCP Guard is different |
204
+ |----------|---------------------------|
205
+ | API gateway | MCP operates at the tool-call layer, not HTTP. MCP Guard understands tool names and MCP's JSON-RPC protocol. |
206
+ | RBAC / IAM | Those gate *users*. MCP Guard gates *agent actions* — the agent is already authenticated, the question is what it's allowed to *do*. |
207
+ | "Just code it" | You could. MCP Guard gives you a declarative config, audit receipts, observe mode, and an approval UI — without changing your server code. |
208
+
209
+ ## What This Is NOT
210
+
211
+ - **Not a dashboard.** It's a proxy. It sits in the data path and enforces rules.
212
+ - **Not a scanner.** It doesn't analyze your code or model outputs. It gates tool calls.
213
+ - **Not compliance theater.** Every decision has a cryptographic receipt.
214
+ - **Not a replacement for good architecture.** It's one layer in a defense-in-depth strategy.
215
+
216
+ Agents shouldn't be able to execute arbitrary actions without an explicit decision point.
217
+
218
+ ## Part of Permission Protocol
219
+
220
+ MCP Guard is a building block of the [Permission Protocol](https://github.com/permission-protocol) — explicit authority for autonomous AI systems.
221
+
222
+ ## License
223
+
224
+ MIT
Binary file
package/bin/mcp-guard ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/src/cli.js');
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ GREEN='\033[0;32m'
5
+ RED='\033[1;31m'
6
+ BOLD='\033[1m'
7
+ DIM='\033[2m'
8
+ NC='\033[0m'
9
+ BG_RED='\033[41m'
10
+ BG_GREEN='\033[42m'
11
+ BLACK='\033[30m'
12
+
13
+ clear
14
+ echo ""
15
+
16
+ # ── BEFORE (red = danger) ──
17
+ echo -e " ${BG_RED}${BLACK}${BOLD} ✖ WITHOUT MCP GUARD ${NC}"
18
+ echo ""
19
+ sleep 0.6
20
+ echo -e " ${DIM}\$${NC} call ${BOLD}delete_user_data${NC}"
21
+ sleep 0.4
22
+ echo -e " ${GREEN}✔ success${NC} — user data deleted"
23
+ echo ""
24
+ sleep 1.5
25
+
26
+ # ── DIVIDER ──
27
+ echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
28
+ echo ""
29
+ sleep 0.4
30
+
31
+ # ── AFTER (green = safe) ──
32
+ echo -e " ${BG_GREEN}${BLACK}${BOLD} ✔ WITH MCP GUARD ${NC}"
33
+ echo ""
34
+ sleep 0.6
35
+ echo -e " ${DIM}\$${NC} call ${BOLD}delete_user_data${NC}"
36
+ sleep 0.4
37
+ echo -e " ${RED}${BOLD}✖ BLOCKED${NC}"
38
+ echo -e " ${DIM}rule: block-delete-user-data${NC}"
39
+ echo ""
40
+ sleep 0.5
41
+ echo -e " ${DIM}{ \"decision\": \"blocked\" }${NC}"
42
+ echo ""
43
+ sleep 2.5
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Tiny MCP server for testing mcp-guard.
5
+ * Responds to tools/call, tools/list, and initialize.
6
+ * Newline-delimited JSON-RPC over stdio.
7
+ */
8
+ const node_readline_1 = require("node:readline");
9
+ const TOOLS = [
10
+ { name: 'send_email', description: 'Send an email', inputSchema: { type: 'object', properties: { to: { type: 'string' }, body: { type: 'string' } } } },
11
+ { name: 'delete_user_data', description: 'Delete all user data', inputSchema: { type: 'object', properties: { user_id: { type: 'string' } } } },
12
+ { name: 'deploy_production', description: 'Deploy to production', inputSchema: { type: 'object', properties: { version: { type: 'string' } } } },
13
+ ];
14
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin });
15
+ rl.on('line', (line) => {
16
+ if (!line.trim())
17
+ return;
18
+ let msg;
19
+ try {
20
+ msg = JSON.parse(line);
21
+ }
22
+ catch {
23
+ return;
24
+ }
25
+ if (msg.method === 'initialize') {
26
+ respond(msg.id, {
27
+ protocolVersion: '2024-11-05',
28
+ capabilities: { tools: {} },
29
+ serverInfo: { name: 'example-server', version: '0.1.0' },
30
+ });
31
+ }
32
+ else if (msg.method === 'tools/list') {
33
+ respond(msg.id, { tools: TOOLS });
34
+ }
35
+ else if (msg.method === 'tools/call') {
36
+ const toolName = msg.params?.name ?? 'unknown';
37
+ respond(msg.id, {
38
+ content: [{ type: 'text', text: `Executed ${toolName} successfully` }],
39
+ });
40
+ }
41
+ else if (msg.method === 'notifications/initialized') {
42
+ // No response needed for notifications
43
+ }
44
+ else {
45
+ respondError(msg.id, -32601, `Method not found: ${msg.method}`);
46
+ }
47
+ });
48
+ function respond(id, result) {
49
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
50
+ }
51
+ function respondError(id, code, message) {
52
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n');
53
+ }
54
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../example/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { Receipt } from './receipt.js';
2
+ export declare function pushReceipt(receipt: Receipt): void;
3
+ export declare function setOnApprove(cb: (id: string) => void): void;
4
+ export declare function startApprovalServer(port: number): void;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pushReceipt = pushReceipt;
4
+ exports.setOnApprove = setOnApprove;
5
+ exports.startApprovalServer = startApprovalServer;
6
+ const node_http_1 = require("node:http");
7
+ const pending_js_1 = require("./pending.js");
8
+ const approval_ui_js_1 = require("./approval-ui.js");
9
+ /** Recent receipts store (ring buffer) */
10
+ const recentReceipts = [];
11
+ const MAX_RECEIPTS = 50;
12
+ function pushReceipt(receipt) {
13
+ recentReceipts.push(receipt);
14
+ if (recentReceipts.length > MAX_RECEIPTS)
15
+ recentReceipts.shift();
16
+ }
17
+ /** Callback invoked when an action is approved — proxy registers this */
18
+ let onApproveCallback = null;
19
+ function setOnApprove(cb) {
20
+ onApproveCallback = cb;
21
+ }
22
+ function parseBody(req) {
23
+ return new Promise((resolve, reject) => {
24
+ const chunks = [];
25
+ req.on('data', (c) => chunks.push(c));
26
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()));
27
+ req.on('error', reject);
28
+ });
29
+ }
30
+ function json(res, status, data) {
31
+ const body = JSON.stringify(data);
32
+ res.writeHead(status, {
33
+ 'Content-Type': 'application/json',
34
+ 'Access-Control-Allow-Origin': '*',
35
+ });
36
+ res.end(body);
37
+ }
38
+ function text(res, status, msg) {
39
+ res.writeHead(status, { 'Content-Type': 'text/plain' });
40
+ res.end(msg);
41
+ }
42
+ function startApprovalServer(port) {
43
+ const server = (0, node_http_1.createServer)(async (req, res) => {
44
+ const url = req.url ?? '/';
45
+ const method = req.method ?? 'GET';
46
+ // CORS preflight
47
+ if (method === 'OPTIONS') {
48
+ res.writeHead(204, {
49
+ 'Access-Control-Allow-Origin': '*',
50
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
51
+ 'Access-Control-Allow-Headers': 'Content-Type',
52
+ });
53
+ res.end();
54
+ return;
55
+ }
56
+ try {
57
+ // GET / — Approval UI
58
+ if (method === 'GET' && url === '/') {
59
+ res.writeHead(200, { 'Content-Type': 'text/html' });
60
+ res.end((0, approval_ui_js_1.getApprovalHTML)());
61
+ return;
62
+ }
63
+ // GET /api/pending
64
+ if (method === 'GET' && url === '/api/pending') {
65
+ const pending = (0, pending_js_1.getPending)().map(p => ({
66
+ id: p.id,
67
+ timestamp: p.timestamp,
68
+ tool_name: p.tool_name,
69
+ tool_args: p.tool_args,
70
+ agent_id: p.agent_id,
71
+ rule_id: p.rule_id,
72
+ status: p.status,
73
+ }));
74
+ json(res, 200, pending);
75
+ return;
76
+ }
77
+ // POST /api/approve/:id
78
+ const approveMatch = url.match(/^\/api\/approve\/([a-f0-9-]+)$/);
79
+ if (method === 'POST' && approveMatch) {
80
+ const id = approveMatch[1];
81
+ const action = (0, pending_js_1.resolveAction)(id, 'approved');
82
+ if (!action) {
83
+ json(res, 404, { error: 'Not found or already resolved' });
84
+ return;
85
+ }
86
+ process.stderr.write(`[mcp-guard] Approved: ${action.tool_name} (${id})\n`);
87
+ // Notify proxy to forward the original request
88
+ if (onApproveCallback)
89
+ onApproveCallback(id);
90
+ json(res, 200, { id, status: 'approved' });
91
+ return;
92
+ }
93
+ // POST /api/deny/:id
94
+ const denyMatch = url.match(/^\/api\/deny\/([a-f0-9-]+)$/);
95
+ if (method === 'POST' && denyMatch) {
96
+ const id = denyMatch[1];
97
+ const action = (0, pending_js_1.resolveAction)(id, 'denied');
98
+ if (!action) {
99
+ json(res, 404, { error: 'Not found or already resolved' });
100
+ return;
101
+ }
102
+ process.stderr.write(`[mcp-guard] Denied: ${action.tool_name} (${id})\n`);
103
+ // Return JSON-RPC error to the waiting caller
104
+ const errResp = JSON.stringify({
105
+ jsonrpc: '2.0',
106
+ id: action.jsonrpcId,
107
+ error: { code: -32002, message: 'Denied by administrator' },
108
+ });
109
+ action.resolve(errResp);
110
+ json(res, 200, { id, status: 'denied' });
111
+ return;
112
+ }
113
+ // GET /api/receipts
114
+ if (method === 'GET' && url === '/api/receipts') {
115
+ json(res, 200, recentReceipts.slice(-50).reverse());
116
+ return;
117
+ }
118
+ text(res, 404, 'Not found');
119
+ }
120
+ catch (err) {
121
+ text(res, 500, err.message ?? 'Internal error');
122
+ }
123
+ });
124
+ server.listen(port, () => {
125
+ process.stderr.write(`[mcp-guard] Approval UI: http://localhost:${port}\n`);
126
+ });
127
+ }
128
+ //# sourceMappingURL=approval-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-server.js","sourceRoot":"","sources":["../../src/approval-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function getApprovalHTML(): string;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getApprovalHTML = getApprovalHTML;
4
+ function getApprovalHTML() {
5
+ return `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title>MCP Guard — Approval Queue</title>
11
+ <style>
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
15
+ background: #0d1117; color: #c9d1d9; line-height: 1.5;
16
+ padding: 24px; max-width: 900px; margin: 0 auto;
17
+ }
18
+ h1 { color: #58a6ff; font-size: 24px; margin-bottom: 4px; }
19
+ .subtitle { color: #8b949e; font-size: 14px; margin-bottom: 24px; }
20
+ .badge {
21
+ display: inline-block; padding: 2px 8px; border-radius: 12px;
22
+ font-size: 12px; font-weight: 600; text-transform: uppercase;
23
+ }
24
+ .badge-pending { background: #d29922; color: #0d1117; }
25
+ .badge-approved { background: #238636; color: #fff; }
26
+ .badge-denied { background: #da3633; color: #fff; }
27
+ .badge-allowed { background: #238636; color: #fff; }
28
+ .badge-blocked { background: #da3633; color: #fff; }
29
+ .badge-held_for_approval { background: #d29922; color: #0d1117; }
30
+ .card {
31
+ border: 1px solid #30363d; border-radius: 8px; padding: 16px;
32
+ margin-bottom: 12px; background: #161b22;
33
+ }
34
+ .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
35
+ .tool-name { font-size: 18px; font-weight: 700; color: #f0f6fc; }
36
+ .meta { font-size: 13px; color: #8b949e; margin-bottom: 8px; }
37
+ .meta span { margin-right: 16px; }
38
+ .args-toggle {
39
+ background: none; border: 1px solid #30363d; color: #58a6ff;
40
+ padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
41
+ margin-bottom: 8px;
42
+ }
43
+ .args-toggle:hover { border-color: #58a6ff; }
44
+ .args-content {
45
+ display: none; background: #0d1117; border: 1px solid #30363d;
46
+ border-radius: 4px; padding: 12px; margin-bottom: 12px;
47
+ font-family: 'SF Mono', Consolas, monospace; font-size: 13px;
48
+ overflow-x: auto; white-space: pre-wrap; word-break: break-all;
49
+ }
50
+ .args-content.open { display: block; }
51
+ .actions { display: flex; gap: 8px; }
52
+ .btn {
53
+ padding: 6px 16px; border-radius: 6px; border: none;
54
+ font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity 0.15s;
55
+ }
56
+ .btn:hover { opacity: 0.85; }
57
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; }
58
+ .btn-approve { background: #238636; color: #fff; }
59
+ .btn-deny { background: #da3633; color: #fff; }
60
+ .empty {
61
+ text-align: center; padding: 40px; color: #8b949e;
62
+ border: 1px dashed #30363d; border-radius: 8px; margin-bottom: 24px;
63
+ }
64
+ h2 { color: #c9d1d9; font-size: 18px; margin: 24px 0 12px; }
65
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
66
+ th { text-align: left; padding: 8px; border-bottom: 2px solid #30363d; color: #8b949e; font-weight: 600; }
67
+ td { padding: 8px; border-bottom: 1px solid #21262d; }
68
+ .pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #3fb950; margin-right: 8px; animation: pulse 2s infinite; }
69
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
70
+ .status-bar { display: flex; align-items: center; font-size: 12px; color: #8b949e; margin-bottom: 16px; }
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <h1>MCP Guard — Approval Queue</h1>
75
+ <p class="subtitle">Human-in-the-loop approval for AI tool calls</p>
76
+ <div class="status-bar"><span class="pulse"></span> Polling every 3s</div>
77
+
78
+ <div id="pending"></div>
79
+ <h2>Recent Decisions</h2>
80
+ <div id="receipts"></div>
81
+
82
+ <script>
83
+ function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
84
+
85
+ function toggleArgs(btn) {
86
+ const c = btn.nextElementSibling;
87
+ c.classList.toggle('open');
88
+ btn.textContent = c.classList.contains('open') ? '▾ Hide arguments' : '▸ Show arguments';
89
+ }
90
+
91
+ async function act(id, action) {
92
+ const btns = document.querySelectorAll('[data-id="'+id+'"]');
93
+ btns.forEach(b => b.disabled = true);
94
+ try {
95
+ const r = await fetch('/api/' + action + '/' + id, { method: 'POST' });
96
+ if (!r.ok) { const e = await r.text(); alert('Error: ' + e); }
97
+ } catch(e) { alert('Network error'); }
98
+ refresh();
99
+ }
100
+
101
+ async function refresh() {
102
+ try {
103
+ const [pRes, rRes] = await Promise.all([
104
+ fetch('/api/pending'), fetch('/api/receipts')
105
+ ]);
106
+ const pending = await pRes.json();
107
+ const receipts = await rRes.json();
108
+
109
+ const pEl = document.getElementById('pending');
110
+ if (pending.length === 0) {
111
+ pEl.innerHTML = '<div class="empty">No pending approvals</div>';
112
+ } else {
113
+ pEl.innerHTML = pending.map(p => \`
114
+ <div class="card">
115
+ <div class="card-header">
116
+ <span class="tool-name">\${esc(p.tool_name)}</span>
117
+ <span class="badge badge-pending">pending</span>
118
+ </div>
119
+ <div class="meta">
120
+ <span>Agent: <strong>\${esc(p.agent_id)}</strong></span>
121
+ <span>Rule: <strong>\${esc(p.rule_id)}</strong></span>
122
+ <span>\${esc(new Date(p.timestamp).toLocaleTimeString())}</span>
123
+ </div>
124
+ <button class="args-toggle" onclick="toggleArgs(this)">▸ Show arguments</button>
125
+ <div class="args-content">\${esc(JSON.stringify(p.tool_args, null, 2))}</div>
126
+ <div class="actions">
127
+ <button class="btn btn-approve" data-id="\${p.id}" onclick="act('\${p.id}','approve')">✅ Approve</button>
128
+ <button class="btn btn-deny" data-id="\${p.id}" onclick="act('\${p.id}','deny')">❌ Deny</button>
129
+ </div>
130
+ </div>
131
+ \`).join('');
132
+ }
133
+
134
+ const rEl = document.getElementById('receipts');
135
+ if (receipts.length === 0) {
136
+ rEl.innerHTML = '<div class="empty">No recent decisions</div>';
137
+ } else {
138
+ rEl.innerHTML = \`<table>
139
+ <thead><tr><th>Time</th><th>Tool</th><th>Decision</th><th>Agent</th><th>Rule</th></tr></thead>
140
+ <tbody>\${receipts.map(r => \`<tr>
141
+ <td>\${esc(new Date(r.timestamp).toLocaleTimeString())}</td>
142
+ <td><strong>\${esc(r.tool_name)}</strong></td>
143
+ <td><span class="badge badge-\${r.decision}">\${esc(r.decision)}</span></td>
144
+ <td>\${esc(r.agent_id)}</td>
145
+ <td>\${esc(r.rule_id || '—')}</td>
146
+ </tr>\`).join('')}</tbody>
147
+ </table>\`;
148
+ }
149
+ } catch(e) { /* retry next cycle */ }
150
+ }
151
+
152
+ refresh();
153
+ setInterval(refresh, 3000);
154
+ </script>
155
+ </body>
156
+ </html>`;
157
+ }
158
+ //# sourceMappingURL=approval-ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-ui.js","sourceRoot":"","sources":["../../src/approval-ui.ts"],"names":[],"mappings":";;AAAA,0CAyJC;AAzJD,SAAgB,eAAe;IAC7B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAuJD,CAAC;AACT,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function main(): void;