使用说明
mcp-guardian
Archived
This project is archived and no longer maintained. The repository is read-only; nothing below will be corrected.
If you came here to bridge a stdio MCP client to an HTTP MCP server, use mcp-bridge instead. That is the half of mcp-guardian that saw real use, extracted and rebuilt: the same Streamable HTTP transport and OAuth support — including pre-registered confidential clients, an https loopback callback, and RFC 8414/7591 discovery — with a single strictly decoded config file and six subcommands in place of fourteen flags.
The governance layer that gives this project its name never found real use. The five gates, hash-chained receipts, meta-tool injection, tool masking and telemetry exporters were built and tested but not adopted in practice, and no receipts were ever produced in day-to-day operation. That is the reason for archiving rather than any defect in the idea; the design is recorded in
docs/for anyone who wants to read it.Known defects, left as they are
These were found while extracting mcp-bridge and are documented here rather than fixed, because the archive is read-only. Anyone reading this code should know about them:
enforcement: "advisory"does not mean "never block". The budget and schema gates ininternal/governance/gates.goignoreEnforcemententirely, somaxCallsandschema: "strict"block calls in advisory mode. The statement in README §Governance Pipeline that advisory violations "are logged but forwarded" is wrong for those two gates.- Only the profile is strictly decoded.
LoadProfilerejects unknown fields;LoadGlobalininternal/config/file.gouses a plainjson.Unmarshal, so a typo in the global config is silently ignored while the same typo in a profile is fatal.- Error messages name flags that no longer exist.
Config.Validateininternal/config/config.gostill suggests--upstream-url,--oauth2-client-idand--oauth2-client-secret, which were removed when configuration moved to profiles.--inspectand--callback-portare documented nowhere. Both work; neither appears in README.md or README.ja.md.mcp-bridge carries none of these: it has no advisory mode, one strictly decoded config file, and tests that pin every flag and command to the usage text.
A governance proxy for MCP (Model Context Protocol) servers, built as a single binary with zero external dependencies.
Inspired by @sovereign-labs/mcp-proxy, reimplemented in Go for supply chain security and operational robustness.
Why
MCP tool servers give AI agents powerful capabilities. Without oversight, agents can repeat failed operations, exhaust resources, or make unauthorized mutations. mcp-guardian sits transparently between the MCP client and server, providing:
- Tamper-evident audit trail -- Every tool call produces a SHA-256 hash-chained receipt
- Failure-based constraint learning -- Automatically blocks retries of the same failed operation
- Budget and convergence controls -- Prevents runaway loops and excessive API calls
- Schema validation -- Validates tool arguments before forwarding
- Authority tracking -- Epoch-based session validity
- Tool masking -- Forcibly hide tools from agents (wildcard patterns supported)
- OpenTelemetry export -- OTLP/HTTP Logs + Traces for enterprise telemetry collection
Features
- Single static binary (~6MB), no runtime dependencies
- Go standard library only -- zero external modules
- Dual transport: stdio (default) and HTTP/SSE (Streamable HTTP)
- MCP Authorization Discovery: auto-discovers OAuth2 endpoints and registers clients dynamically -- no manual OAuth app setup required
- OAuth2 authentication: client_credentials and authorization_code (browser login) with automatic token refresh
- Browser login:
--loginauto-discovers OAuth2, registers a client, opens browser, stores tokens - External token command: integrate with
gcloud,vault, or any CLI tool - 401 auto-retry: transparent token refresh on authentication failure
- Fail-fast on unforwardable requests: if a client request can't be sent upstream (e.g. the stored OAuth token has expired and there is no refresh token), the proxy returns a JSON-RPC error to the client carrying the reason (
...access token expired ... run --login again) instead of leaving the client to hang until its own timeout - Hash-chained receipt ledger (JSONL, verifiable)
- Receipt auto-purge: configurable retention period, OTLP/Splunk for long-term storage
- 5-gate governance pipeline
- 5 injected meta-tools for agent self-governance
- Post-session analysis CLI (view, verify, explain)
- Webhook notifications (generic, Discord, Telegram)
- Pluggable telemetry: OTLP/HTTP and Splunk HEC drivers (run in parallel)
- Tool masking with glob patterns (
--mask,--profile) - Two-tier configuration (system global config + server profiles)
- Per-process receipt files for safe parallel execution (no file locks)
Install
# From source
git clone https://github.com/nlink-jp/mcp-guardian.git
cd mcp-guardian
make install
# Or specify prefix
make install PREFIX=$HOME/.local
Quick Start
Using server profiles (recommended)
Create a profile at ~/.config/mcp-guardian/profiles/filesystem.json:
{
"name": "filesystem",
"upstream": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
"governance": { "enforcement": "advisory" }
}
mcp-guardian --profile filesystem
SSE server with OAuth2 (auto-discovery):
{
"name": "atlassian",
"upstream": {
"transport": "sse",
"url": "https://mcp.atlassian.com/v1/mcp"
}
}
No OAuth2 configuration needed -- --login auto-discovers endpoints and registers a client:
# First time: discovers OAuth2, registers client, opens browser
mcp-guardian --login atlassian
# Subsequent runs: tokens auto-refresh
mcp-guardian --profile atlassian
# Add to Claude Code
claude mcp add atlassian -- mcp-guardian --profile atlassian
Post-session analysis
mcp-guardian --profile atlassian --view
mcp-guardian --profile atlassian --view --tool write_file --outcome error
mcp-guardian --profile atlassian --verify
mcp-guardian --profile atlassian --explain
mcp-guardian --profile atlassian --receipts
CLI Reference
# Proxy mode
mcp-guardian --profile <name|path>
# Profile management
--profile <name|path> Server profile (name or path)
--profiles List available profiles
--login <name|path> OAuth2 browser login (auto-discovers endpoints)
# Global config
--config <path> Global config file (telemetry, defaults)
# Analysis (requires --profile or --state-dir)
--view Receipt timeline
--verify Hash chain verification
--explain Session narrative
--receipts Compact summary
--state-dir <dir> Override state directory
--tool <name> Filter by tool name (for --view)
--outcome <outcome> Filter by outcome (for --view)
--limit <n> Limit receipts (for --view)
# Info
--version Show version
All transport, authentication, governance, and masking settings are configured in server profiles (JSON). See examples/profiles/ for templates.
Governance Pipeline
Every tools/call passes through 5 gates:
- Budget -- Rejects if call count exceeds
maxCalls - Schema -- Validates arguments against cached
inputSchema - Constraint -- Blocks if tool+target matches a prior failure (TTL: 1 hour)
- Authority -- Verifies session epoch matches authority epoch
- Convergence -- Detects loops (3+ same failure, 5+ same tool+target in 2 min)
In strict mode, any gate failure blocks the call. In advisory mode, violations are logged but forwarded.
Meta-Tools
The proxy injects 5 governance tools that agents can call:
| Tool | Description |
|---|---|
governance_status |
Inspect controller ID, epoch, constraints, receipt depth |
governance_bump_authority |
Advance epoch (invalidates current session) |
governance_declare_intent |
Declare goal + predicates for attribution |
governance_clear_intent |
Clear declared intent |
governance_convergence_status |
Inspect loop detection state |
Tool Masking
Hide tools from agents. Requires enforcement: "strict" to take effect. In strict mode, masked tools are removed from tools/list responses and calls return a generic "tool not found" error, preventing agents from knowing the tool exists. In advisory mode, masked tools are logged to stderr but remain visible and callable.
In the profile:
{
"governance": { "enforcement": "strict" },
"mask": ["write_*", "delete_*"]
}
Patterns use glob syntax (* matches any characters, ? matches one character).
Configuration
Two-tier configuration separates system-wide telemetry from per-server policies:
~/.config/mcp-guardian/
config.json # System global (telemetry + org defaults)
profiles/
github-mcp.json # Server profile
filesystem.json
state/
github-mcp/ # Per-profile state (auto-created)
receipts-1712400000000-12345.jsonl
controller.json
authority.json
filesystem/
...
See the examples/ directory for ready-to-use templates.
System global config
Auto-discovered from ~/.config/mcp-guardian/config.json, or specified with --config.
Shared across all MCP server instances. Ideal for MDM/EMM deployment.
{
"telemetry": {
"otlp": {
"endpoint": "http://otel-collector:4318",
"headers": { "Authorization": "Bearer org-token" },
"batchSize": 10,
"batchTimeout": 5000
},
"webhooks": ["https://hooks.slack.com/..."]
},
"defaults": {
"enforcement": "strict",
"schema": "warn"
}
}
The legacy format (top-level otlp/webhooks) is still supported for backward compatibility.
Server profiles (--profile)
Per-MCP-server configuration. Stored in ~/.config/mcp-guardian/profiles/ or referenced by path.
SSE server with auto-discovery (minimal):
{
"name": "atlassian",
"upstream": { "transport": "sse", "url": "https://mcp.atlassian.com/v1/mcp" }
}
stdio server:
{
"name": "my-server",
"upstream": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
},
"governance": {
"enforcement": "advisory",
"schema": "strict",
"maxCalls": 50
},
"mask": ["write_*", "execute_*"]
}
SSE with OAuth2 client_credentials (M2M):
{
"name": "api-server",
"upstream": { "transport": "sse", "url": "http://mcp.example.com/mcp" },
"auth": {
"oauth2": {
"tokenUrl": "https://auth.example.com/oauth2/token",
"clientId": "my-client",
"clientSecret": "my-secret",
"scopes": ["mcp:read", "mcp:write"]
}
},
"governance": { "enforcement": "strict" }
}
SSE with OAuth2 authorization_code (explicit config -- usually not needed, --login auto-discovers):
{
"name": "github-mcp",
"upstream": { "transport": "sse", "url": "https://mcp.github.com/sse" },
"auth": {
"oauth2": {
"flow": "authorization_code",
"authorizeUrl": "https://github.com/login/oauth/authorize",
"tokenUrl": "https://github.com/login/oauth/access_token",
"clientId": "my-app",
"scopes": ["repo"]
}
}
}
External token command:
{
"name": "gcp-server",
"upstream": {
"transport": "sse",
"url": "http://mcp.example.com/mcp"
},
"auth": {
"tokenCommand": {
"command": "gcloud",
"args": ["auth", "print-access-token"]
}
}
}
Priority order
Defaults → Global config (auto-discovered or --config) → Profile (--profile) → CLI flags
CLI flags always win. Sensitive values (e.g., OAuth2 secrets) in profiles avoid exposure via ps.
MCP Client Integration (.mcp.json)
From the MCP client's perspective, mcp-guardian is always a stdio process -- regardless of whether the upstream MCP server uses stdio or SSE. Profiles encapsulate all transport and auth complexity.
{
"mcpServers": {
"filesystem": {
"command": "mcp-guardian",
"args": ["--profile", "filesystem"]
},
"github": {
"command": "mcp-guardian",
"args": ["--profile", "github-mcp"]
}
}
}
For SSE servers requiring authentication, run mcp-guardian --login <profile> once. OAuth2 endpoints and client registration are handled automatically.
Telemetry Export
Pluggable telemetry with two built-in drivers that can run in parallel. Zero external dependencies.
| Driver | Use case | Config key |
|---|---|---|
| OTLP/HTTP | CloudWatch, GCP, Grafana Cloud, Datadog, etc. | telemetry.otlp |
| Splunk HEC | Splunk Enterprise / Cloud (direct, no collector) | telemetry.splunk |
Both drivers can run simultaneously. Local receipts auto-purge after maxReceiptAgeDays (default: 7) -- telemetry backends are the durable store.
For setup guides (AWS, GCP, Grafana Cloud, Datadog, Splunk, self-hosted), see docs/en/reference/otlp-setup.md.
Architecture
Agent (Claude, GPT, etc.)
| stdin/stdout (JSON-RPC 2.0)
mcp-guardian
| Transport interface
+-- stdio: stdin/stdout pipe to child process (default)
+-- sse: HTTP POST + SSE stream to remote server
Upstream MCP Server
For detailed architecture documentation, see docs/en/reference/architecture.md.
OAuth2 authorization_code login works two ways:
- Auto-discovery (default). If the upstream MCP server's
authorization server supports RFC 7591 Dynamic Client Registration,
mcp-guardian --login <profile>discovers everything from a singleupstream.url. - Manual setup for providers that do not support DCR (Slack,
GitHub Apps, Microsoft Entra ID, most enterprise SaaS). Pre-register
an OAuth app, then configure
auth.oauth2in the profile — includingcallbackPort(fixed loopback port matching the provider-registered redirect URI) and optionallyclientAuthMethod("post"/"basic"/"none"). Worked walkthrough in docs/en/reference/oauth2-manual-setup.md. Slack example profile at examples/profiles/slack.json.
Non-expiring tokens (e.g. Slack without token rotation). Some providers issue an access token that never expires and return no
refresh_tokenand noexpires_in. In that casetokens.jsonrecords"refresh_token": ""and"expires_at": 0— this is expected, not an error. mcp-guardian uses the token indefinitely and only reports an auth failure when the upstream actually rejects it (HTTP 401), at which point it tells you to run--loginagain. (ADR-0003.)
State directory
Default: ~/.config/mcp-guardian/state/<profile-name>/. Override per profile with stateDir or via --state-dir.
| File | Contents |
|---|---|
receipts-<ms>-<pid>.jsonl |
Per-process append-only hash-chained audit trail |
constraints.json |
Learned failure fingerprints with TTL |
controller.json |
Stable controller UUID |
authority.json |
Epoch + session binding + genesis hash |
intent.json |
Currently declared intent |
Each proxy process writes to its own receipt file (receipts-<unixmilli>-<pid>.jsonl), avoiding file locks and enabling safe parallel execution. Analysis commands (--view, --verify, etc.) aggregate all receipt files automatically. Legacy receipts.jsonl files are still read for backward compatibility.
Build
make build # Build to dist/
make install # Install to /usr/local/bin
make test # Run unit tests
make check # Lint + test
make integration-test # Run OTLP integration tests (requires podman/docker)
make otel-up # Start OTel Collector for manual testing
make otel-down # Stop OTel Collector
make clean # Clean build artifacts
make help # Show all targets
License
MIT License. Copyright (c) 2026 magifd2
Acknowledgments
This project owes its core design to @sovereign-labs/mcp-proxy by Born14.
The original Node.js/TypeScript implementation pioneered the idea of a transparent governance proxy for MCP servers -- inserting an auditing layer between AI agents and tool servers without either side knowing. The key concepts we adopted from that work include:
- Hash-chained receipt ledger -- treating every tool call as an immutable, tamper-evident record (like git commits for agent actions)
- Failure-based constraint learning -- fingerprinting failed calls and automatically blocking identical retries within a TTL window
- Authority tracking with epochs -- a monotonic counter proving which controller was active during each call
- Pure-function governance gates -- separating governance math from I/O so invariants can be verified in isolation
mcp-guardian is a ground-up reimplementation in Go, not a fork or a port, but the architectural blueprint and the insight that MCP tool calls need governance -- not just logging -- came directly from Born14's work. We chose Go and zero external dependencies to address supply chain security concerns in security-sensitive environments, but the "what to build" was already answered by @sovereign-labs/mcp-proxy.
If you find mcp-guardian useful, please also star the original project that made it possible.