The Open-Source MCP Security Gateway for AI Agents
Built by Technosive Ltd.
⚠️ Alpha Software — v0.1SentinelMCP is currently in Alpha. The project is under active development and APIs, configuration formats, and graph behaviors may change in future releases without advance notice.
Production warning: This software is provided as-is. While we strive for security, an Alpha-stage proxy should not be your sole defense in a highly regulated production environment without thorough testing. Use at your own risk.
We actively seek early adopters and feedback. If you encounter issues, have suggestions, or want to contribute — please open an issue. Your input directly shapes the roadmap.
SentinelMCP is a security enforcement engine for the Model Context Protocol (MCP) that secures AI agent tool calls at runtime. It provides inspection, policy enforcement, PII/secret redaction, and audit logging — sitting between your AI agents and the tools they invoke.
Built in high-performance Go with runtime-native graph orchestration, interrupt/resume capabilities, and sub-millisecond enforcement — enabling human-in-the-loop approval workflows without custom plumbing.
SentinelMCP can be deployed in two ways, depending on your architecture:
A standalone sidecar binary that intercepts HTTP/SSE MCP traffic. Works with any language (Python, TypeScript, Go, etc.) and any agent framework. Zero code changes required — just route your MCP traffic through the proxy.
AI Agent (any language) → SentinelMCP Proxy → MCP Server
A Go library imported directly into your application. Wraps MCP tool calls in-memory with zero-copy graph orchestration. Provides sub-millisecond latency and deep context awareness — no network hop, no separate process.
Go AI Application → SentinelMCP SDK (in-process) → MCP Server
This dual-mode architecture is SentinelMCP's key differentiator: competitors like Permit or Envoy only offer network proxies. With the Inline SDK, Go applications get the same security enforcement at a fraction of the latency.
- MCP Proxy (Sidecar) — Drop-in HTTP/SSE proxy for any MCP client. Transparent to existing agent frameworks.
- Inline SDK (Go) — Native Go module for in-process enforcement. Sub-millisecond overhead on the Allow path (19μs p99).
- Policy Engine — Local YAML-based policy definitions with hot-reloading. Risk levels (low/medium/high) map to enforcement actions (allow/redact/block/interrupt).
- Data Loss Prevention (DLP) — Regex-based PII and secret redaction in tool arguments and responses. 6 built-in patterns (private keys, passwords, API keys, credit cards, SSNs, emails) plus custom regex support.
- Transport & Auth Security — A security gateway must harden its own transport too. Loopback-plaintext-default with fail-closed hardening: opt-in inbound TLS, API-key authentication, an admin token gating approval-resume, strict mode that rejects plaintext/IP-literal upstreams, upstream cert pinning, outbound credential injection, and an egress allowlist. See Transport Security.
- Human-in-the-Loop (HITL) — Interrupt high-risk tool calls via generic Webhooks and resume via a local API endpoint. BoltDB-backed checkpoints for durable interrupt/resume.
- Audit Logging — Structured JSON logging to
stdoutand native OpenTelemetry (OTel) integration for SIEM pipelines. - State Management — BoltDB-backed checkpoints for durable interrupt/resume across process restarts.
SentinelMCP is built with a strict separation of concerns to ensure maintainability, testability, and a seamless Open-Core experience.
The gateway/ package defines all core interfaces and domain types (Policy, DLP, Risk, Audit) with zero dependencies on the underlying Eino framework or MCP transport libraries. The adapter/eino/ package is the only package that imports Eino types.
This architectural boundary provides three major benefits:
- Framework Agnostic: If the underlying orchestration framework ever needs to change, only a new adapter is required; the core security engine remains untouched.
- Independently Testable: Core domain logic (policy evaluation, DLP redaction, risk analysis) can be unit-tested cleanly without spinning up MCP servers or LLM graphs.
- Architecturally Enforced Open-Core: Enterprise implementations (Teams/Slack HITL, Nightfall DLP, Control Plane APIs) simply swap into the
GatewayConfigvia interfaces. Zero changes to this OSS codebase are required.
1. Start the sidecar. The fastest path is the bundled three-service demo — an upstream MCP server with demo tools, the sidecar, and a test client that exercises every enforcement flow:
docker compose up --build --abort-on-container-exitFor a persistent sidecar against your own upstream, run a single container (or locally: go run ./cmd/sentinelmcp -config policies.yaml):
docker run --name sentinelmcp \
-p 8080:8080 -p 9090:9090 \
-v ./policies.yaml:/etc/sentinelmcp/config.yaml \
ghcr.io/technosiveuk-ui/sentinelmcp:latestPort 8080 is the MCP proxy; 9090 is the admin API (health probes and the approval-resume endpoint used by high-risk interrupts). Add -d to run detached, then follow logs with docker logs -f sentinelmcp. The example config below binds loopback, which suits a local run; a non-loopback bind (e.g. 0.0.0.0 inside a container) requires TLS or the --insecure-dev-mode flag — see Transport Security.
2. Define your policy (policies.yaml):
schema_version: "1.0"
global:
default_risk: low # risk for tools with no explicit entry (set medium for a stricter default)
redaction_mask: "***REDACTED***"
sidecar:
listen_addr: "localhost:8080" # loopback bind → plaintext HTTP is safe here
health_addr: "localhost:9090" # admin server (health + approval-resume); loopback enforced
transport: streamable_http
strict: false # DEMO ONLY — the local upstream below is plaintext. Production
# MUST set strict: true and dial https:// upstreams.
admin_token: "" # gates /api/v1/approval/resume — set via SENTINELMCP_ADMIN_TOKEN
# tls: # opt-in inbound TLS; required when listen_addr is non-loopback
# cert_file: "/etc/sentinelmcp/tls.crt"
# key_file: "/etc/sentinelmcp/tls.key"
upstream_servers: # your MCP tool servers — at least one is required
- name: my-tools
url: "http://localhost:3001/mcp"
auth:
api_keys: {} # inbound key → principal; enforced on every call when non-empty
# e.g. { "key-abc": "agent-prod" } via Authorization: Bearer / X-API-Key
tools:
read_file:
risk: low # allow — args and response are still DLP-scanned
redact_patterns: [PASSWORD, API_KEY]
write_file:
risk: medium # redact — sensitive argument fields are masked before the tool runs
exec_command:
risk: high # interrupt — pauses for human approval, then runs or blocks
require_approval: true
approval_reason: "Shell commands can modify system state"
"db_*": # glob patterns are supported
risk: high
require_approval: true
dlp_patterns: # six patterns are built in (PRIVATE_KEY, PASSWORD, API_KEY,
INTERNAL_HOSTNAME: # CREDIT_CARD, SSN, EMAIL); define your own here as needed
regex: '\b[a-z]+\.internal\.company\.com\b'
type: pii3. Point your MCP client at the proxy:
http://localhost:8080/mcp
The proxy speaks standard Streamable HTTP MCP, so any client — Python, TypeScript, or Go — connects to that URL with no SDK changes. Point upstream_servers at your own MCP tool server(s) and the proxy enforces the policy above on every call.
Secure tool calls in-process with a small builder API — no sidecar, no network hop, sub-millisecond overhead. Full guide: docs/INLINE-SDK.md.
Requires v0.2.0+. The Inline SDK (
sdk/package) shipped in v0.2.0.
package main
import (
"context"
"fmt"
"log"
"github.com/technosiveuk-ui/sentinelmcp/gateway"
"github.com/technosiveuk-ui/sentinelmcp/sdk"
)
func main() {
// Register plain Go functions as secured tools.
invoker := sdk.NewFuncInvoker().
Register("read_file", func(_ context.Context, args map[string]any) (string, error) {
return fmt.Sprintf("contents of %v", args["path"]), nil
}).
Register("exec_command", func(_ context.Context, args map[string]any) (string, error) {
return fmt.Sprintf("ran %v", args["cmd"]), nil
})
// Build the pipeline. StrictDefaults routes unknown tools to redact.
pipeline, err := sdk.New(invoker).
WithRisk("exec_command", gateway.RiskHigh). // interrupt for approval
StrictDefaults().
Build()
if err != nil {
log.Fatal(err)
}
// Every call is inspected, DLP-scanned, policy-checked, and audited.
result, err := pipeline.Run(context.Background(), "read_file", map[string]any{
"path": "/etc/config.yaml",
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Result:", result)
}Run the complete allow / redact / interrupt example:
go run ./examples/inline-sdkflowchart LR
A[AI Agent] --> S[SentinelMCP]
S --> I[Inspect Tool Call]
I --> D{Policy Decision}
D -->|Allow| E[Execute Tool]
D -->|Redact| R[Redact PII] --> E
D -->|Block| X[Return Error]
D -->|Interrupt| H[Human Approval]
H -->|Approved| E
H -->|Denied| X
E --> O[Inspect Response]
O --> A
E --> MCP[MCP Server]
MCP --> O
Pipeline flow:
- Inspect — Serialize tool arguments, run DLP scanning, look up risk level in policy DB
- Policy Decision — Route by risk: low→allow, medium→redact, high→interrupt for approval, or block
- Execute — Call the upstream MCP tool (with redacted arguments if applicable)
- Inspect Response — DLP-scan the tool response, redact findings, emit structured audit event
SentinelMCP is a security gateway, so its own transport surface is hardened, not left plaintext. It follows a loopback-plaintext-default model: on a loopback address (127.0.0.1, ::1, localhost) the sidecar may run plaintext HTTP with no auth — the host boundary carries trust, like redis or postgres on a local socket. The moment it binds a non-loopback interface or dials a network upstream, it fail-closes to TLS and authentication instead of degrading to plaintext.
- Strict mode (default
true) rejects plaintexthttp://and IP-literal upstream URLs at config load. - Inbound TLS is opt-in on loopback, required off-loopback (
sidecar.tls). - API-key auth (
auth.api_keys) validates every inbound call in constant time; the resolved principal reaches the policy and audit layers. - Admin token (
sidecar.admin_token/SENTINELMCP_ADMIN_TOKEN) always gates the approval-resume endpoint, even on loopback. - Upstream identity can be pinned per-server (
ca_bundle,server_name,pinned_sha256);InsecureSkipVerifyis never set. - Outbound credentials are injected from a mode-
0600secrets.yamlor env, and never logged. - Egress allowlist (
sidecar.egress_allowlist) bounds which upstream hosts may be dialed. - Secrets at rest: a group/world-readable
config.yamlcontaining secrets, or asecrets.yaml, is refused at load.
Full rationale, the bind-policy matrix, and the complete fail-closed contract live in docs/transport-security.md. The two open-core seams — gateway/auth.Authenticator and gateway/secrets.Provider — are where Enterprise implementations (mTLS, OAuth2/OIDC, HashiCorp Vault) attach behind the same pure interfaces.
sentinelmcp/
├── gateway/ # Core domain (ZERO Eino imports)
│ ├── types.go # GatewayContext, AuditEvent, DLPFinding, RiskLevel, Decision
│ ├── graph.go # Pipeline, ToolInvoker, GatewayConfig, MetricsRecorder
│ ├── policy.go # Policy, RiskDB interfaces + DefaultPolicy, YAMLRiskDB
│ ├── policy_action.go # Action-based PolicySet (allow/redact/block/interrupt by rule)
│ ├── policy_reload.go # Hot policy reload (atomic swap on config change)
│ ├── redact.go # DLPScanner, Redactor, ScanArgs + RegexDLPScanner, DefaultRedactor
│ ├── dlp_multi.go # MultiDLPScanner (compose regex + external)
│ ├── dlp_external.go # DLPEndpoint interface + ExternalDLPScanner adapter
│ ├── audit.go # AuditEmitter interface + Stdout/FileAuditEmitter
│ ├── audit_sink.go # AuditSink + WriterAuditSink + CompositeAuditEmitter
│ ├── metrics.go # MetricsRecorder interface + NOPMetricsRecorder
│ ├── webhook_approval.go # WebhookApprovalProvider + CLIApprovalProvider
│ ├── auth/ # Authenticator interface + APIKeyAuthenticator (open-core seam)
│ │ └── auth.go # pure — no net/http; key→principal, constant-time
│ └── secrets/ # secrets.Provider interface + FileEnvProvider (open-core seam)
│ └── secrets.go # pure — resolves credentials_ref; secrets.yaml (0600) or env
├── sdk/ # Inline SDK: ergonomic builder over gateway + adapter
│ ├── builder.go # Builder API (New, With*, StrictDefaults, Build)
│ ├── func_invoker.go # FuncInvoker — secure plain Go functions
│ └── defaults.go # Convenience constructors (BuiltinDLP, StdoutAudit, …)
├── adapter/eino/ # Eino framework adapter (ONLY package with Eino imports)
│ ├── graph.go # BuildGraph() → 3-node graph with interrupt/resume
│ ├── otel_metrics.go # OTelMetricsRecorder (counters + histograms)
│ ├── otel_audit_emitter.go # OTelAuditEmitter (audit → span events)
│ └── bolt_checkpoint.go # BoltCheckPointStore
├── adapter/sidecar/ # Sidecar proxy adapters
│ ├── invoker.go # SidecarInvoker (routes to upstream MCP servers)
│ └── discovery.go # DiscoverTools (upstream MCP server discovery)
├── adapter/siem/ # SIEM audit sink implementations
│ ├── splunk_hec.go # SplunkHECSink (batching + retry)
│ └── file_rotation.go # FileRotationSink (size-based + gzip)
├── config/ # YAML config loading, validation, hot-reload
│ ├── config.go # Load, Validate, ToRiskDB, ToDLPPatterns
│ └── watcher.go # ConfigWatcher (fsnotify + debounce)
├── cmd/
│ ├── sentinelmcp/ # Sidecar proxy binary
│ │ ├── main.go # Bootstrap: config → discover → pipeline → proxy → admin
│ │ ├── proxy.go # Proxy MCP server wrapping the pipeline
│ │ ├── health.go # Admin server: /healthz, /readyz, /api/v1/approval/resume
│ │ └── config.go # GatewayConfig wiring (audit sinks, approval, OTel)
│ ├── upstream/ # Demo upstream MCP server (3 tools)
│ ├── testclient/ # Demo test client (3 enforcement flows)
│ └── demo/ # In-process demo (4 MCP servers)
├── examples/
│ └── inline-sdk/ # Runnable allow/redact/interrupt SDK demo
├── Dockerfile # Multi-stage distroless build
├── docker-compose.yml # One-command demo
└── config/
├── config.yaml # Default config
└── docker-config.yaml # Docker-specific config
| Feature | OSS (This Repo — Apache 2.0) | Enterprise (Commercial) |
|---|---|---|
| Deployment | Sidecar Proxy + Inline Go SDK | Centralized Control Plane |
| Policies | Local YAML with hot-reload | Centralized API/DB, Web UI |
| DLP | Regex-based (6 built-in + custom) | Enterprise DLP connectors (API-based) |
| HITL Approval | Generic Webhook + CLI | Corporate communication channels |
| Audit | JSON stdout + OTel | SIEM Aggregation, Compliance PDFs |
| Auth | API keys (constant-time) | SSO, RBAC, multi-tenant |
| Transport | Loopback-plaintext-default; opt-in TLS, strict mode, cert pinning, egress allowlist | mTLS, OAuth2/OIDC, mesh egress |
| Secrets | File (0600) / env | HashiCorp Vault, AWS/GCP Secret Manager |
| State | BoltDB (local) | Distributed (Postgres) |
Note: The Enterprise Control Plane connects to the sidecar/SDK by implementing the
gateway/interfaces (Policy,DLPScanner,AuditEmitter,ApprovalProvider, etc.). Zero changes to this OSS codebase are required.
Benchmarks on Apple M1 Pro (Go 1.26):
| Path | Latency | Allocations |
|---|---|---|
| Allow (critical path) | 19μs | 182 |
| Redact | 19μs | 185 |
| Interrupt | 33μs | 303 |
| DLP scan | 9.3μs | 0 |
| RiskDB lookup | 71ns | 0 |
| Policy decide | 193ns | 4 |
| NFR | Target | Validated By |
|---|---|---|
| p99 latency | ≤500μs Allow | BenchmarkPipeline_Allow — 19μs (26× under budget) |
| Memory | <10MB heap per 1k calls | TestMemory_* — 0.73MB concurrent |
| Concurrency | 100+ concurrent, no deadlock | TestLoad_* — 1000 concurrent, 0 deadlocks |
| Default-deny | Block on all failures | TestDegradation_* — all error paths block |
90+ tests across 6 packages, 7 benchmarks, all passing with -race:
# Run all tests
go test ./... -race -count=1
# NFR validation
go test ./adapter/eino/ -run "TestLoad_|TestMemory_|TestDegradation_" -race -v
# Sidecar E2E
go test ./cmd/sentinelmcp/ -race -v
# Benchmarks
go test ./adapter/eino/ -bench=. -benchmemThe full enforcement surface is also validated live against a real MCP upstream,
not only in unit tests. With the demo upstream running and a sidecar configured
to proxy it (see Quickstart; a plaintext localhost upstream
needs sidecar.strict: false), the bundled client exercises every path:
go run ./cmd/upstream # demo upstream (echo_message, filesystem_read, exec_command)
go run ./cmd/sentinelmcp -config ... # sidecar proxy (point upstream_servers at :3001)
go run ./cmd/testclient # allow / redact / interrupt+resume — exits non-zero on any failureThe live run validates: allow (low-risk pass-through), redact (medium-risk
DLP masking of secrets in the response), interrupt + resume (high-risk calls
pause for approval and complete via the admin resume API), fail-closed auth
(the resume endpoint rejects missing/invalid admin tokens with 401), and hot
policy reload (policy changes apply on config save, no restart).
Contributions are welcome! Please read CONTRIBUTING.md for details.
All contributors must sign off on their commits (DCO — Developer Certificate of Origin) to ensure they have the right to submit their code under the Apache 2.0 license.
Signed-off-by: Your Name <your.email@example.com>
Open Source Code:
The code in this repository, including the gateway/ interfaces and adapter/ implementations, is licensed under the Apache 2.0 License. You are free to use, modify, and distribute this code in accordance with that license.
Enterprise Extensions & Interface Implementations:
This repository defines extensibility interfaces (e.g., Policy, DLPScanner, ApprovalProvider, AuditEmitter) in the gateway/ package to allow for custom integrations. While the interfaces themselves are Apache 2.0, proprietary implementations of these interfaces provided by Technosive Ltd. (such as the SentinelMCP Enterprise Control Plane, corporate communication HITL integrations, and advanced DLP connectors) are licensed separately under a commercial license and are not included in this repository.
Implementing these interfaces in your own code does not require you to license your implementations under Apache 2.0, nor does it obligate you to open-source your custom integrations.
Trademark: "SentinelMCP" and the SentinelMCP logo are trademarks of Technosive Ltd. The Apache 2.0 license does not grant you the right to use the SentinelMCP trademark to endorse or promote products or services derived from this software without prior written permission from Technosive Ltd.
