@mcp-guardian/server 0.4.0 → 0.6.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/README.md +116 -8
- package/dist/auth/auth-types.d.ts +40 -0
- package/dist/auth/auth-types.d.ts.map +1 -0
- package/dist/auth/auth-types.js +5 -0
- package/dist/auth/auth-types.js.map +1 -0
- package/dist/auth/oauth.d.ts +25 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +96 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/session-cache.d.ts +47 -0
- package/dist/auth/session-cache.d.ts.map +1 -0
- package/dist/auth/session-cache.js +91 -0
- package/dist/auth/session-cache.js.map +1 -0
- package/dist/cli.js +23 -5
- package/dist/cli.js.map +1 -1
- package/dist/database/database-interface.d.ts +17 -0
- package/dist/database/database-interface.d.ts.map +1 -0
- package/dist/database/database-interface.js +2 -0
- package/dist/database/database-interface.js.map +1 -0
- package/dist/database/history-db.d.ts.map +1 -1
- package/dist/database/history-db.js.map +1 -1
- package/dist/database/postgres-db.d.ts +18 -0
- package/dist/database/postgres-db.d.ts.map +1 -0
- package/dist/database/postgres-db.js +118 -0
- package/dist/database/postgres-db.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/policy/policy-engine.d.ts.map +1 -1
- package/dist/policy/policy-engine.js +21 -0
- package/dist/policy/policy-engine.js.map +1 -1
- package/dist/policy/policy-types.d.ts +9 -0
- package/dist/policy/policy-types.d.ts.map +1 -1
- package/dist/policy/policy-watcher.d.ts +24 -0
- package/dist/policy/policy-watcher.d.ts.map +1 -0
- package/dist/policy/policy-watcher.js +68 -0
- package/dist/policy/policy-watcher.js.map +1 -0
- package/dist/proxy/proxy-manager.d.ts +3 -1
- package/dist/proxy/proxy-manager.d.ts.map +1 -1
- package/dist/proxy/proxy-manager.js +10 -3
- package/dist/proxy/proxy-manager.js.map +1 -1
- package/dist/proxy/proxy-server.d.ts +20 -8
- package/dist/proxy/proxy-server.d.ts.map +1 -1
- package/dist/proxy/proxy-server.js +201 -37
- package/dist/proxy/proxy-server.js.map +1 -1
- package/dist/utils/circuit-breaker.d.ts +29 -0
- package/dist/utils/circuit-breaker.d.ts.map +1 -0
- package/dist/utils/circuit-breaker.js +81 -0
- package/dist/utils/circuit-breaker.js.map +1 -0
- package/dist/utils/structured-logger.d.ts +1 -1
- package/dist/utils/structured-logger.d.ts.map +1 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ MCP Guardian scans your [Model Context Protocol](https://modelcontextprotocol.io
|
|
|
22
22
|
- [One-Off Scan](#one-off-scan)
|
|
23
23
|
- [CLI Reference](#cli-reference)
|
|
24
24
|
- [`mcp-guardian proxy`](#mcp-guardian-proxy)
|
|
25
|
+
- [Policy Engine (v0.4+)](#policy-engine-v04)
|
|
25
26
|
- [`mcp-guardian scan`](#mcp-guardian-scan)
|
|
26
27
|
- [`mcp-guardian audit`](#mcp-guardian-audit)
|
|
27
28
|
- [`mcp-guardian health`](#mcp-guardian-health)
|
|
@@ -30,6 +31,7 @@ MCP Guardian scans your [Model Context Protocol](https://modelcontextprotocol.io
|
|
|
30
31
|
- [Available Tools](#available-tools)
|
|
31
32
|
- [Available Resources & Prompts](#available-resources--prompts)
|
|
32
33
|
- [CI/CD Integration](#cicd-integration)
|
|
34
|
+
- [Production Deployment (K8s + Helm)](#production-deployment-k8s--helm)
|
|
33
35
|
- [Docker](#docker)
|
|
34
36
|
- [Architecture](#architecture)
|
|
35
37
|
- [Data Flow (Proxy → DB → Audit)](#data-flow-proxy--db--audit)
|
|
@@ -38,6 +40,7 @@ MCP Guardian scans your [Model Context Protocol](https://modelcontextprotocol.io
|
|
|
38
40
|
- [Pricing Models](#pricing-models)
|
|
39
41
|
- [Environment Variables](#environment-variables)
|
|
40
42
|
- [Development](#development)
|
|
43
|
+
- [SECURITY.md](#securitymd)
|
|
41
44
|
- [FAQ](#faq)
|
|
42
45
|
- [Roadmap](#roadmap)
|
|
43
46
|
- [License](#license)
|
|
@@ -50,10 +53,17 @@ As MCP adoption grows, so does the attack surface. MCP servers run arbitrary com
|
|
|
50
53
|
|
|
51
54
|
MCP Guardian provides:
|
|
52
55
|
|
|
56
|
+
- **Active policy enforcement (v0.4+)** — YAML-configurable policy engine that blocks, flags, or passes every `tools/call` in real time based on tool allowlists/denylists, regex patterns, rate limits, and token budgets
|
|
53
57
|
- **Security auditing** — CVE scanning (OSV.dev + NVD), hardcoded secret detection, typo-squatting detection, command injection detection, and TLS validation
|
|
54
58
|
- **Real cost tracking** — Proxy interceptor that captures actual `tools/call` traffic and counts tokens via `tiktoken` (o200k_base encoding) — no estimates, no mocks
|
|
55
59
|
- **Health monitoring** — Live JSON-RPC 2.0 handshake probes with latency, success rate, tool count, and context pressure analysis
|
|
56
60
|
- **Agent-native** — Runs as an MCP server so your AI assistant can self-audit its own infrastructure
|
|
61
|
+
- **Enterprise SIEM logging (v0.4+)** — Structured JSON logs via pino with request-ID tracing, policy decision audit trails, and block events at WARN level
|
|
62
|
+
- **Session-based replay protection (v0.6.0)** — Short-lived 5-min session tokens prevent JWT replay attacks. Nonce tracking detects token reuse
|
|
63
|
+
- **Hot-reload policies (v0.6.0)** — File watcher atomically swaps policy engine on YAML changes — no restart needed
|
|
64
|
+
- **Circuit breaker (v0.5.2)** — 3-state circuit breaker protects upstream MCP servers from cascading failures
|
|
65
|
+
- **OAuth 2.1 / OIDC (v0.5.0)** — JWT validation with OIDC Discovery, bearer token extraction, agent identity mapping
|
|
66
|
+
- **RBAC (v0.5.1)** — Scope-based and client-ID-based access control in policy engine
|
|
57
67
|
|
|
58
68
|
---
|
|
59
69
|
|
|
@@ -69,6 +79,7 @@ MCP Guardian provides:
|
|
|
69
79
|
| **Typo-Squat Detection** | Levenshtein distance matching against 24 known official MCP packages |
|
|
70
80
|
| **Secret Scanning** | 6 regex patterns for hardcoded API keys, tokens, private keys, passwords, GitHub tokens, OpenAI keys |
|
|
71
81
|
| **Command Validation** | Flags dangerous patterns (path traversal, shell chaining, `rm -rf`, `curl`/`wget` in commands, and more) |
|
|
82
|
+
| **🔴 Active Policy Engine (v0.4+)** | YAML-configurable rules: tool allowlist/denylist, regex pattern blocking, rate limiting, token budgets. Operates in `audit` (passive), `warn` (flag only), or `block` (active enforcement) modes |
|
|
72
83
|
| **Scoring** | Weighted 0–100 security score with actionable recommendations |
|
|
73
84
|
|
|
74
85
|
### 💰 Cost Audit (`audit_costs`)
|
|
@@ -99,7 +110,7 @@ MCP Guardian provides:
|
|
|
99
110
|
- **Graceful Shutdown** — SIGINT/SIGTERM handlers flush DB and close connections
|
|
100
111
|
- **Batched DB Writes** — 1s debounced flush reduces I/O by 10x
|
|
101
112
|
- **Alert Thresholds** — 6 CLI flags with exit codes 1/2 for CI/CD integration
|
|
102
|
-
- **GitHub Actions CI** — Node 18/20/22 matrix,
|
|
113
|
+
- **GitHub Actions CI** — Node 18/20/22 matrix, 74 tests across 11 suites
|
|
103
114
|
|
|
104
115
|
---
|
|
105
116
|
|
|
@@ -192,15 +203,64 @@ mcp-guardian report --format markdown --output audit-report.md
|
|
|
192
203
|
|
|
193
204
|
### `mcp-guardian proxy`
|
|
194
205
|
|
|
195
|
-
Start the MCP proxy interceptor
|
|
206
|
+
Start the MCP proxy interceptor with optional active policy enforcement.
|
|
196
207
|
|
|
197
208
|
```bash
|
|
209
|
+
# Audit-only (passive)
|
|
198
210
|
mcp-guardian proxy --config ./cline_mcp_settings.json
|
|
211
|
+
|
|
212
|
+
# Active blocking with default policy
|
|
213
|
+
mcp-guardian proxy --config ./cline_mcp_settings.json --policy ./default-policy.yaml
|
|
214
|
+
|
|
215
|
+
# Active blocking with custom policy + mode override
|
|
216
|
+
mcp-guardian proxy --config ./cline_mcp_settings.json --policy ./my-policy.yaml --blocking-mode block
|
|
199
217
|
```
|
|
200
218
|
|
|
201
219
|
| Option | Description |
|
|
202
220
|
|---|---|
|
|
203
221
|
| `-c, --config <path>` | Path to MCP config file |
|
|
222
|
+
| `--policy <path>` | Path to policy YAML file (enables active blocking) |
|
|
223
|
+
| `--blocking-mode <mode>` | Override policy mode: `audit` (passive), `warn` (flag), `block` (enforce) |
|
|
224
|
+
|
|
225
|
+
### Policy Engine (v0.4+)
|
|
226
|
+
|
|
227
|
+
The policy engine evaluates every intercepted `tools/call` before it reaches the MCP server. Define rules in YAML:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
# my-policy.yaml
|
|
231
|
+
version: "1.0"
|
|
232
|
+
policy:
|
|
233
|
+
mode: block
|
|
234
|
+
rules:
|
|
235
|
+
- name: "deny-shell-tools"
|
|
236
|
+
action: block
|
|
237
|
+
tools: { deny: ["execute_command", "bash", "sh", "eval", "exec"] }
|
|
238
|
+
- name: "block-injection"
|
|
239
|
+
action: block
|
|
240
|
+
patterns:
|
|
241
|
+
- "rm\\s+-rf"
|
|
242
|
+
- "curl\\s|wget\\s"
|
|
243
|
+
- ";\\s*\\w"
|
|
244
|
+
- "&&|\\|\\|"
|
|
245
|
+
- name: "rate-limit"
|
|
246
|
+
action: flag
|
|
247
|
+
maxCallsPerMinute: 60
|
|
248
|
+
- name: "token-budget"
|
|
249
|
+
action: flag
|
|
250
|
+
maxTokens: 50000
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Blocked calls** return a JSON-RPC 2.0 error to the client:
|
|
254
|
+
```json
|
|
255
|
+
{"jsonrpc":"2.0","id":"abc-123","error":{"code":-32001,"message":"Blocked by MCP Guardian policy: Tool 'execute_command' is explicitly denied"}}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Policy modes:**
|
|
259
|
+
| Mode | Behavior |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `audit` | Pass all calls; log decisions only (passive) |
|
|
262
|
+
| `warn` | Downgrade `block` actions to `flag`; log warnings |
|
|
263
|
+
| `block` | Full active enforcement — blocked calls never reach the MCP server |
|
|
204
264
|
|
|
205
265
|
### `mcp-guardian scan`
|
|
206
266
|
|
|
@@ -349,6 +409,41 @@ Run MCP Guardian in CI to catch issues before deployment:
|
|
|
349
409
|
|
|
350
410
|
---
|
|
351
411
|
|
|
412
|
+
## Production Deployment (K8s + Helm)
|
|
413
|
+
|
|
414
|
+
See the full guide at **[deploy/PRODUCTION.md](deploy/PRODUCTION.md)**.
|
|
415
|
+
|
|
416
|
+
### Quick Helm Install
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
# Install from local chart
|
|
420
|
+
helm install mcp-guardian ./deploy/helm/mcp-guardian \
|
|
421
|
+
--set config.policy.mode=block \
|
|
422
|
+
--set config.mcpConfigPath=/etc/mcp-guardian/cline_mcp_settings.json
|
|
423
|
+
|
|
424
|
+
# Or from the repo (future)
|
|
425
|
+
helm repo add mcp-guardian https://rudraneel93.github.io/mcp-guardian
|
|
426
|
+
helm install mcp-guardian mcp-guardian/mcp-guardian
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Key Features
|
|
430
|
+
- **Helm chart** with ConfigMap-backed policies, PVC persistence, and safe defaults
|
|
431
|
+
- **Fail-closed** by default (block traffic if proxy crashes) — configurable to fail-open
|
|
432
|
+
- **Sidecar injection pattern** documented for stdio MCP servers
|
|
433
|
+
- **Scaling guide** with CPU/memory recommendations per traffic level
|
|
434
|
+
- **Pod Disruption Budget** for HA, anti-affinity for multi-AZ
|
|
435
|
+
- **SIEM integration** via pino structured JSON logs (Splunk, Datadog, Elasticsearch)
|
|
436
|
+
|
|
437
|
+
### Performance Overhead
|
|
438
|
+
|
|
439
|
+
| Scenario | p50 | p99 | Overhead |
|
|
440
|
+
|----------|-----|-----|----------|
|
|
441
|
+
| Direct MCP (no proxy) | 5ms | 7ms | — |
|
|
442
|
+
| Proxy (no policy) | 27ms | 77ms | +25.78ms |
|
|
443
|
+
| Proxy (blocking policy) | 27ms | 74ms | +25.93ms |
|
|
444
|
+
|
|
445
|
+
Policy engine adds **~0.15ms** — negligible. The ~26ms is Node.js child process stdio overhead.
|
|
446
|
+
|
|
352
447
|
## Docker
|
|
353
448
|
|
|
354
449
|
A Docker image is available for running the proxy in containerized environments.
|
|
@@ -414,7 +509,7 @@ mcp-guardian/
|
|
|
414
509
|
│ ├── scoring.ts # Shared scoring utility
|
|
415
510
|
│ └── logger.ts # Colored console logger with log levels
|
|
416
511
|
│
|
|
417
|
-
tests/ #
|
|
512
|
+
tests/ # 74 tests across 11 suites (Vitest)
|
|
418
513
|
├── config-parser.test.ts
|
|
419
514
|
├── secret-scanner.test.ts
|
|
420
515
|
├── auth-prober.test.ts
|
|
@@ -541,7 +636,7 @@ npm install
|
|
|
541
636
|
npm run dev # Watch mode with tsx
|
|
542
637
|
npm run build # Compile TypeScript
|
|
543
638
|
npm run lint # Type check (tsc --noEmit)
|
|
544
|
-
npm test #
|
|
639
|
+
npm test # 74 tests across 11 suites (Vitest)
|
|
545
640
|
npm run test:watch # Watch mode
|
|
546
641
|
|
|
547
642
|
# Contributing
|
|
@@ -617,13 +712,26 @@ Token counting uses `tiktoken` with the `o200k_base` encoding (used by GPT-4o an
|
|
|
617
712
|
- [x] Token-bucket rate limiter (OSV + NVD)
|
|
618
713
|
- [x] TLS certificate validation
|
|
619
714
|
- [x] Command injection validation (10 suspicious patterns)
|
|
620
|
-
- [x]
|
|
715
|
+
- [x] Active policy engine — YAML-based pass/block/flag with allowlists, regex, rate limiting, token budgets
|
|
716
|
+
- [x] Structured JSON logging (pino) for SIEM ingestion
|
|
717
|
+
- [x] STRIDE threat model (SECURITY.md)
|
|
718
|
+
- [x] 74 tests (11 suites)
|
|
621
719
|
- [x] GitHub Actions CI (Node 18/20/22 matrix)
|
|
720
|
+
- [x] Performance benchmarks (p50: 5ms baseline, +25.78ms proxy overhead, +0.15ms policy)
|
|
721
|
+
- [x] Helm chart + production deployment guide (K8s, fail-open/closed, sidecar pattern, scaling)
|
|
622
722
|
- [x] Published to npm as [`@mcp-guardian/server`](https://www.npmjs.com/package/@mcp-guardian/server)
|
|
723
|
+
- [x] OAuth 2.1 / OIDC proxy authentication (v0.5.0)
|
|
724
|
+
- [x] RBAC — scope & client-ID-based access control (v0.5.1)
|
|
725
|
+
- [x] Circuit breaker — 3-state protection for upstream servers (v0.5.2)
|
|
726
|
+
- [x] Per‑client rate limiting (v0.5.2)
|
|
727
|
+
- [x] Consistent SIEM fields — requestId, authnSuccess, authzAllowed (v0.5.2)
|
|
728
|
+
- [x] Session binding — replay protection via 5‑min session tokens (v0.6.0)
|
|
729
|
+
- [x] Hot‑reload policies — chokidar file watcher (v0.6.0)
|
|
730
|
+
- [ ] OPA integration for Rego policies
|
|
623
731
|
- [ ] Web dashboard for historical trends
|
|
624
|
-
- [ ] Slack/Discord alerting
|
|
625
|
-
- [ ]
|
|
626
|
-
- [ ] Multi-user proxy
|
|
732
|
+
- [ ] Slack/Discord alerting
|
|
733
|
+
- [ ] Prometheus metrics endpoint
|
|
734
|
+
- [ ] Multi-user proxy
|
|
627
735
|
|
|
628
736
|
---
|
|
629
737
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 / OIDC authentication types for MCP Guardian proxy.
|
|
3
|
+
*/
|
|
4
|
+
export interface AuthConfig {
|
|
5
|
+
/** OIDC issuer URL (e.g., https://accounts.google.com) */
|
|
6
|
+
issuer: string;
|
|
7
|
+
/** Expected audience claim in JWT */
|
|
8
|
+
audience: string;
|
|
9
|
+
/** Whether authentication is required (fail-closed) or optional (fail-open) */
|
|
10
|
+
required: boolean;
|
|
11
|
+
/** JWKS URI override (default: auto-discovered from issuer) */
|
|
12
|
+
jwksUri?: string;
|
|
13
|
+
/** Clock tolerance in seconds for JWT validation */
|
|
14
|
+
clockTolerance?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface AgentIdentity {
|
|
17
|
+
/** Subject claim (sub) — unique agent identifier */
|
|
18
|
+
sub: string;
|
|
19
|
+
/** Client ID from the token */
|
|
20
|
+
clientId?: string;
|
|
21
|
+
/** Scopes granted to this agent */
|
|
22
|
+
scopes?: string[];
|
|
23
|
+
/** Issuer of the token */
|
|
24
|
+
issuer: string;
|
|
25
|
+
/** Token expiry timestamp */
|
|
26
|
+
expiresAt?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface AuthValidationResult {
|
|
29
|
+
valid: boolean;
|
|
30
|
+
identity?: AgentIdentity;
|
|
31
|
+
error?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface OIDCDiscovery {
|
|
34
|
+
issuer: string;
|
|
35
|
+
jwks_uri: string;
|
|
36
|
+
authorization_endpoint?: string;
|
|
37
|
+
token_endpoint?: string;
|
|
38
|
+
scopes_supported?: string[];
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=auth-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-types.d.ts","sourceRoot":"","sources":["../../src/auth/auth-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,OAAO,CAAC;IAClB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-types.js","sourceRoot":"","sources":["../../src/auth/auth-types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AuthConfig, AuthValidationResult, OIDCDiscovery } from './auth-types.js';
|
|
2
|
+
export declare class OAuthValidator {
|
|
3
|
+
private config;
|
|
4
|
+
private jwks;
|
|
5
|
+
private cachedDiscovery;
|
|
6
|
+
constructor(config: AuthConfig);
|
|
7
|
+
/**
|
|
8
|
+
* Perform OIDC discovery to fetch JWKS URI from issuer.
|
|
9
|
+
*/
|
|
10
|
+
discover(): Promise<OIDCDiscovery>;
|
|
11
|
+
/**
|
|
12
|
+
* Initialize JWKS from discovery or explicit URI.
|
|
13
|
+
*/
|
|
14
|
+
init(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Validate a JWT bearer token and extract agent identity.
|
|
17
|
+
*/
|
|
18
|
+
validate(token: string): Promise<AuthValidationResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Extract Bearer token from Authorization header.
|
|
21
|
+
*/
|
|
22
|
+
static extractToken(authorizationHeader?: string): string | null;
|
|
23
|
+
getConfig(): AuthConfig;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAiB,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGjG,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,IAAI,CAA2D;IACvE,OAAO,CAAC,eAAe,CAA8B;gBAEzC,MAAM,EAAE,UAAU;IAI9B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,aAAa,CAAC;IAiBxC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAkC5D;;OAEG;IACH,MAAM,CAAC,YAAY,CAAC,mBAAmB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMhE,SAAS,IAAI,UAAU;CAGxB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 / OIDC JWT Validator for MCP Guardian proxy.
|
|
3
|
+
*
|
|
4
|
+
* Validates bearer tokens from MCP requests against an OIDC provider.
|
|
5
|
+
* Uses OIDC Discovery (RFC 8414) to auto-configure JWKS endpoint.
|
|
6
|
+
* Supports Client Credentials flow (most common for server-to-agent MCP).
|
|
7
|
+
*/
|
|
8
|
+
import * as jose from 'jose';
|
|
9
|
+
import { StructuredLogger } from '../utils/structured-logger.js';
|
|
10
|
+
export class OAuthValidator {
|
|
11
|
+
config;
|
|
12
|
+
jwks = null;
|
|
13
|
+
cachedDiscovery = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Perform OIDC discovery to fetch JWKS URI from issuer.
|
|
19
|
+
*/
|
|
20
|
+
async discover() {
|
|
21
|
+
if (this.cachedDiscovery)
|
|
22
|
+
return this.cachedDiscovery;
|
|
23
|
+
const discoveryUrl = `${this.config.issuer}/.well-known/openid-configuration`;
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(discoveryUrl);
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(`OIDC discovery failed: HTTP ${res.status}`);
|
|
28
|
+
const meta = (await res.json());
|
|
29
|
+
this.cachedDiscovery = meta;
|
|
30
|
+
StructuredLogger.info({ event: 'oidc_discovery', issuer: this.config.issuer, jwks_uri: meta.jwks_uri });
|
|
31
|
+
return meta;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
StructuredLogger.logError({ event: 'oidc_discovery_error', serverName: 'oauth', error: `Failed to discover OIDC config: ${err?.message}` });
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialize JWKS from discovery or explicit URI.
|
|
40
|
+
*/
|
|
41
|
+
async init() {
|
|
42
|
+
let jwksUri = this.config.jwksUri;
|
|
43
|
+
if (!jwksUri) {
|
|
44
|
+
const discovery = await this.discover();
|
|
45
|
+
jwksUri = discovery.jwks_uri;
|
|
46
|
+
}
|
|
47
|
+
this.jwks = jose.createRemoteJWKSet(new URL(jwksUri));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Validate a JWT bearer token and extract agent identity.
|
|
51
|
+
*/
|
|
52
|
+
async validate(token) {
|
|
53
|
+
if (!this.jwks) {
|
|
54
|
+
try {
|
|
55
|
+
await this.init();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { valid: false, error: `Auth provider unreachable: ${err?.message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!this.jwks) {
|
|
62
|
+
return { valid: false, error: 'JWKS not initialized' };
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const { payload } = await jose.jwtVerify(token, this.jwks, {
|
|
66
|
+
issuer: this.config.issuer,
|
|
67
|
+
audience: this.config.audience,
|
|
68
|
+
clockTolerance: this.config.clockTolerance || 30,
|
|
69
|
+
});
|
|
70
|
+
const identity = {
|
|
71
|
+
sub: payload.sub || 'unknown',
|
|
72
|
+
clientId: payload.client_id || payload.azp,
|
|
73
|
+
scopes: payload.scope ? String(payload.scope).split(' ') : undefined,
|
|
74
|
+
issuer: payload.iss || this.config.issuer,
|
|
75
|
+
expiresAt: payload.exp ? payload.exp * 1000 : undefined,
|
|
76
|
+
};
|
|
77
|
+
return { valid: true, identity };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { valid: false, error: `JWT validation failed: ${err?.message}` };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract Bearer token from Authorization header.
|
|
85
|
+
*/
|
|
86
|
+
static extractToken(authorizationHeader) {
|
|
87
|
+
if (!authorizationHeader)
|
|
88
|
+
return null;
|
|
89
|
+
const match = authorizationHeader.match(/^Bearer\s+(.+)$/i);
|
|
90
|
+
return match ? match[1] : null;
|
|
91
|
+
}
|
|
92
|
+
getConfig() {
|
|
93
|
+
return this.config;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { AgentIdentity } from './auth-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Session cache for replay protection.
|
|
4
|
+
* After a JWT is validated once, a short-lived session token is issued.
|
|
5
|
+
* Subsequent calls must include this session token, not the raw JWT.
|
|
6
|
+
* This prevents replay of captured JWTs within their expiry window.
|
|
7
|
+
*
|
|
8
|
+
* In production, replace with Redis for multi-replica HA.
|
|
9
|
+
*/
|
|
10
|
+
export interface SessionEntry {
|
|
11
|
+
token: string;
|
|
12
|
+
identity: AgentIdentity;
|
|
13
|
+
nonce: string;
|
|
14
|
+
createdAt: number;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class SessionCache {
|
|
18
|
+
private sessions;
|
|
19
|
+
private usedNonces;
|
|
20
|
+
private readonly sessionTtlMs;
|
|
21
|
+
private readonly nonceTtlMs;
|
|
22
|
+
constructor(sessionTtlMs?: number, nonceTtlMs?: number);
|
|
23
|
+
/**
|
|
24
|
+
* Create a session after successful JWT validation.
|
|
25
|
+
* Returns a session token the client must use for subsequent calls.
|
|
26
|
+
* The JWT cannot be replayed because:
|
|
27
|
+
* 1. We track used nonces (jti or sub+iat)
|
|
28
|
+
* 2. We issue a session token with a short (5min) TTL
|
|
29
|
+
*/
|
|
30
|
+
createSession(identity: AgentIdentity, jwtNonce?: string): SessionEntry;
|
|
31
|
+
/**
|
|
32
|
+
* Validate a session token.
|
|
33
|
+
* Returns the agent identity if valid, null if expired/not found.
|
|
34
|
+
*/
|
|
35
|
+
validateSession(token: string): AgentIdentity | null;
|
|
36
|
+
/**
|
|
37
|
+
* Check if a JWT nonce has been used (replay detection).
|
|
38
|
+
*/
|
|
39
|
+
isNonceUsed(nonce: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Revoke a session (e.g., on logout or suspicious activity).
|
|
42
|
+
*/
|
|
43
|
+
revokeSession(token: string): void;
|
|
44
|
+
private cleanup;
|
|
45
|
+
get size(): number;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=session-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cache.d.ts","sourceRoot":"","sources":["../../src/auth/session-cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,YAAY,GAAE,MAAsB,EAAE,UAAU,GAAE,MAAuB;IAOrF;;;;;;OAMG;IACH,aAAa,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY;IAuBvE;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAUpD;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAInC;;OAEG;IACH,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlC,OAAO,CAAC,OAAO;IAqBf,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { Logger } from '../utils/logger.js';
|
|
3
|
+
export class SessionCache {
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
usedNonces = new Set();
|
|
6
|
+
sessionTtlMs;
|
|
7
|
+
nonceTtlMs;
|
|
8
|
+
constructor(sessionTtlMs = 5 * 60 * 1000, nonceTtlMs = 10 * 60 * 1000) {
|
|
9
|
+
this.sessionTtlMs = sessionTtlMs;
|
|
10
|
+
this.nonceTtlMs = nonceTtlMs;
|
|
11
|
+
// Cleanup expired entries every 60s
|
|
12
|
+
setInterval(() => this.cleanup(), 60000);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create a session after successful JWT validation.
|
|
16
|
+
* Returns a session token the client must use for subsequent calls.
|
|
17
|
+
* The JWT cannot be replayed because:
|
|
18
|
+
* 1. We track used nonces (jti or sub+iat)
|
|
19
|
+
* 2. We issue a session token with a short (5min) TTL
|
|
20
|
+
*/
|
|
21
|
+
createSession(identity, jwtNonce) {
|
|
22
|
+
const nonce = jwtNonce || `${identity.sub}:${Date.now()}:${randomUUID()}`;
|
|
23
|
+
// Prevent nonce replay
|
|
24
|
+
if (this.usedNonces.has(nonce)) {
|
|
25
|
+
Logger.warn(`[session-cache] Replay detected: nonce ${nonce}`);
|
|
26
|
+
}
|
|
27
|
+
this.usedNonces.add(nonce);
|
|
28
|
+
const token = `mcp_guardian_session_${randomUUID()}`;
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const entry = {
|
|
31
|
+
token,
|
|
32
|
+
identity,
|
|
33
|
+
nonce,
|
|
34
|
+
createdAt: now,
|
|
35
|
+
expiresAt: now + this.sessionTtlMs,
|
|
36
|
+
};
|
|
37
|
+
this.sessions.set(token, entry);
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate a session token.
|
|
42
|
+
* Returns the agent identity if valid, null if expired/not found.
|
|
43
|
+
*/
|
|
44
|
+
validateSession(token) {
|
|
45
|
+
const entry = this.sessions.get(token);
|
|
46
|
+
if (!entry)
|
|
47
|
+
return null;
|
|
48
|
+
if (Date.now() > entry.expiresAt) {
|
|
49
|
+
this.sessions.delete(token);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return entry.identity;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if a JWT nonce has been used (replay detection).
|
|
56
|
+
*/
|
|
57
|
+
isNonceUsed(nonce) {
|
|
58
|
+
return this.usedNonces.has(nonce);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Revoke a session (e.g., on logout or suspicious activity).
|
|
62
|
+
*/
|
|
63
|
+
revokeSession(token) {
|
|
64
|
+
this.sessions.delete(token);
|
|
65
|
+
}
|
|
66
|
+
cleanup() {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
// Clean expired sessions
|
|
69
|
+
for (const [token, entry] of this.sessions) {
|
|
70
|
+
if (now > entry.expiresAt) {
|
|
71
|
+
this.sessions.delete(token);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Clean expired nonces (keep for nonceTtlMs to detect replays)
|
|
75
|
+
// This is simplified — in production, use a time-sorted structure
|
|
76
|
+
if (this.usedNonces.size > 10000) {
|
|
77
|
+
// Full sweep
|
|
78
|
+
const entries = Array.from(this.sessions.values());
|
|
79
|
+
const validTokens = new Set(entries.map(e => e.token));
|
|
80
|
+
for (const token of this.sessions.keys()) {
|
|
81
|
+
if (!validTokens.has(token))
|
|
82
|
+
this.sessions.delete(token);
|
|
83
|
+
}
|
|
84
|
+
this.usedNonces.clear();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
get size() {
|
|
88
|
+
return this.sessions.size;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=session-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cache.js","sourceRoot":"","sources":["../../src/auth/session-cache.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ReportGenerator } from './reporter/report-generator.js';
|
|
|
7
7
|
import { calculateOverallScore } from './utils/scoring.js';
|
|
8
8
|
import { ProxyManager } from './proxy/proxy-manager.js';
|
|
9
9
|
import { PolicyEngine } from './policy/policy-engine.js';
|
|
10
|
+
import { OAuthValidator } from './auth/oauth.js';
|
|
10
11
|
import { createContainer } from './container.js';
|
|
11
12
|
// ── Shared helpers ────────────────────────────────────────────────────
|
|
12
13
|
function loadConfigs(options) {
|
|
@@ -40,7 +41,7 @@ const program = new Command();
|
|
|
40
41
|
program
|
|
41
42
|
.name('mcp-guardian')
|
|
42
43
|
.description('Security, cost, and health audit for MCP infrastructure')
|
|
43
|
-
.version('0.
|
|
44
|
+
.version('0.6.0');
|
|
44
45
|
program
|
|
45
46
|
.command('scan')
|
|
46
47
|
.description('Run security scan on MCP servers')
|
|
@@ -184,10 +185,13 @@ program
|
|
|
184
185
|
});
|
|
185
186
|
program
|
|
186
187
|
.command('proxy')
|
|
187
|
-
.description('Start MCP Guardian proxy
|
|
188
|
+
.description('Start MCP Guardian proxy with optional OAuth 2.1 authentication and active policy enforcement')
|
|
188
189
|
.option('-c, --config <path>', 'Path to MCP config file')
|
|
189
190
|
.option('--policy <path>', 'Path to policy YAML file (enables active blocking)')
|
|
190
191
|
.option('--blocking-mode <mode>', 'Override policy mode: audit (passive), warn (flag), block (enforce)', 'block')
|
|
192
|
+
.option('--auth-issuer <url>', 'OIDC issuer URL for JWT validation (e.g., https://accounts.google.com)')
|
|
193
|
+
.option('--auth-audience <aud>', 'Expected audience claim in JWT')
|
|
194
|
+
.option('--auth-required', 'Require authentication for all tool calls (fail-closed)', false)
|
|
191
195
|
.action(async (opts) => {
|
|
192
196
|
const paths = opts.config ? [opts.config] : ConfigParser.findConfigPaths();
|
|
193
197
|
if (paths.length === 0) {
|
|
@@ -199,6 +203,21 @@ program
|
|
|
199
203
|
console.error(chalk.yellow('No servers found in config.'));
|
|
200
204
|
process.exit(0);
|
|
201
205
|
}
|
|
206
|
+
// Configure OAuth 2.1 if --auth-issuer provided
|
|
207
|
+
let authValidator;
|
|
208
|
+
if (opts.authIssuer) {
|
|
209
|
+
if (!opts.authAudience) {
|
|
210
|
+
console.error(chalk.red('--auth-audience is required when --auth-issuer is set'));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const authConfig = {
|
|
214
|
+
issuer: opts.authIssuer,
|
|
215
|
+
audience: opts.authAudience,
|
|
216
|
+
required: opts.authRequired || false,
|
|
217
|
+
};
|
|
218
|
+
authValidator = new OAuthValidator(authConfig);
|
|
219
|
+
console.error(chalk.green(`OAuth 2.1 enabled: ${authConfig.issuer} (audience: ${authConfig.audience})${authConfig.required ? ' [REQUIRED]' : ' [OPTIONAL]'}`));
|
|
220
|
+
}
|
|
202
221
|
// Load policy config if --policy flag provided
|
|
203
222
|
let policyEngine;
|
|
204
223
|
if (opts.policy) {
|
|
@@ -207,7 +226,6 @@ program
|
|
|
207
226
|
const { load } = await import('js-yaml');
|
|
208
227
|
const policyYaml = readFileSync(opts.policy, 'utf-8');
|
|
209
228
|
const policyConfig = load(policyYaml);
|
|
210
|
-
// Override mode from CLI flag if specified
|
|
211
229
|
if (opts.blockingMode && ['audit', 'warn', 'block'].includes(opts.blockingMode)) {
|
|
212
230
|
policyConfig.policy.mode = opts.blockingMode;
|
|
213
231
|
}
|
|
@@ -221,10 +239,10 @@ program
|
|
|
221
239
|
}
|
|
222
240
|
}
|
|
223
241
|
else {
|
|
224
|
-
console.error(chalk.dim('No policy file specified — running in audit-only mode
|
|
242
|
+
console.error(chalk.dim('No policy file specified — running in audit-only mode'));
|
|
225
243
|
}
|
|
226
244
|
const db = new HistoryDatabase();
|
|
227
|
-
const manager = new ProxyManager(db, policyEngine);
|
|
245
|
+
const manager = new ProxyManager(db, policyEngine, authValidator);
|
|
228
246
|
await manager.startAll(servers);
|
|
229
247
|
console.error(chalk.green('MCP Guardian proxy running. Press Ctrl+C to stop.'));
|
|
230
248
|
const cleanup = () => { manager.stopAll(); db.close(); process.exit(0); };
|