← Projects

secu-mcp

The public lab of pythagoras-sec: a generic MCP reverse proxy that refuses any server unable to prove its workload identity, and enforces default-deny on everything an authenticated server can do.

Status
Active
Started
Sept 2026
Stack
Go · SPIFFE/SPIRE · mTLS · MCP · k3s · GCP
Code

Problem. Standard MCP implementations trust a server once it is configured. If an agent or a server is compromised, nothing stands between it and tool execution. secu-mcp makes the answer public as a standalone, reusable proxy.

Architecture. Open-source MCP reverse proxy, over HTTP or stdio, that fails closed:

  • Identity: SPIFFE ID checked over mTLS, or a service-mesh sidecar assertion.
  • Tool policy: default deny; a refused call never reaches the upstream server. Sensitive calls wait for human approval (webhook, or a signed JWT bound to the exact call).
  • Tool pinning: SHA-256 of each tool’s name, description and schema; a tool that drifts is removed and quarantined.
  • Untrusted data: responses are redacted and wrapped in <untrusted_data>.
  • Auditability: HMAC-chained JSONL log.

Validation. go test mints real SVIDs in memory and runs real TLS handshakes and MCP calls, with no cluster needed: the rogue server’s handler never runs. The full lab runs on k3s and SPIRE on GCP.


What it is

secu-mcp is the open part of pythagoras-sec, published on 4 September 2026. It packages the four locks as a reverse proxy (cmd/mcp-proxy) that sits in front of any MCP server, over HTTP or stdio. Every mechanism fails closed when it is not configured.

Proof without infrastructure

The security logic is proven by the test suite, independently of any cluster: internal/testca mints real SVIDs in memory, and the tests go through real TLS handshakes and real MCP calls.

go test -v ./...

Among other things, the suite shows that the rogue server’s handler never runs.

What the proxy enforces

MechanismPackageBehaviour
Identityinternal/identityIDENTITY_MODE=spiffe terminates mTLS and checks the SPIFFE ID; mesh trusts an Istio/Envoy sidecar and verifies the X-Forwarded-Client-Cert assertion; none exists for development and says so loudly.
Tool policyinternal/toolpolicyDefault deny. A refused call never reaches the upstream session.
Human approvalinternal/approvalA real-time webhook pauses the JSON-RPC request until Slack, Jira or ServiceNow answers, or an operator-signed JWT (TTL of 15 min or less) bound to the exact call by sha256(tool + args). Every decision goes to an HMAC-chained JSONL audit log.
Tool pinninginternal/toolpinSHA-256 of each tool’s name, description and schema in tools.lock.json. A tool whose hash drifts is removed and quarantined (ERR_TOOL_SCHEMA_MUTATED).
Untrusted datainternal/envelope, internal/scrubResponses are redacted (tokens, secrets, emails, routable IPs) and sealed in <untrusted_data>. Tool descriptions are redacted too: a hidden instruction there is the same injection, one message earlier.
JSON-RPC surfaceinternal/proxyAny method outside the allowed matrix is refused. Server-initiated sampling and elicitation are dropped by default.

stdio transport

On a desktop host, an MCP server is a subprocess: no network, so no handshake to check. The risk moves to the integrity of the binary. mcp-proxy -stdio -- <command> wraps the server and UPSTREAM_SHA256 pins the binary’s hash before it is spawned.