Subtitle: The MCP gateway security conversation has been almost entirely about governance — access control, rate limits, observability. Almost nobody talks about the one thing auditors actually ask for: cryptographic evidence that a gateway did its job. Here's the gap, why it exists, and how deterministic rule scanning + Ed25519-signed receipts close it.
TL;DR
Most MCP gateways answer "can we block?" with policies, rate limits, and logs. Almost none answer "did we block it, and can you prove it?" — because proving it requires a tamper-evident, offline-verifiable record, and that needs cryptography, not just log lines.
correctover-mcp-gateway@1.0.0 (npm) is an Apache-2.0, zero-runtime-dependency TypeScript gateway that pairs 16 deterministic bidirectional rules (SEC-001–008 inbound, OUT-001–008 outbound) with Ed25519-signed receipts you can verify offline with nothing but a public key. This post walks the reasoning behind that design and shows the actual API — no invented function names, everything below is what ships.
1. The blind spot nobody is talking about
The Model Context Protocol (MCP) is becoming the connective tissue of agentic AI. A host (Claude, Cursor, an internal agent service) calls a set of MCP servers, and those servers reach into databases, codebases, cloud APIs, file systems, and other agents. Every one of those hops is a boundary where a malicious prompt, a poisoned tool description, or an over-eager tool output can cross from "model text" into "system action."
So the security conversation around MCP has been intense — and almost entirely focused on governance:
- Who may call which tool (RBAC, per-tool policies, API keys)
- How much (rate limits, token budgets, cost ceilings)
- Where traffic goes (routing, gateways, proxies)
- What happened (logs, traces, observability)
That's all real and necessary. But there's a question nobody on that list answers, and it's the one that dominates every security review, every incident post-mortem, every compliance audit:
"After the fact, can you prove the gateway blocked the dangerous call? Can a third party verify that claim without trusting you?"
Logs are not evidence. Logs are assertions by the system that wrote them — mutable, partial, and controlled by whoever you're trying to verify. When the question becomes "did the exfiltration actually get stopped, or did the rule silently fail?", an answer of "the logs say so" is not something an auditor can act on.
That gap — between operating a control and producing evidence about that control — is the blind spot. And it's a strange blind spot, because in every other corner of security (TLS, code signing, SBOMs, certificate transparency), cryptography is the default answer to "how do we prove it happened?" Only in the MCP gateway space is the audit record still mostly... logs.
2. What mainstream MCP gateways actually do
As of August 2026, based on public documentation and reporting, the major players in the MCP gateway / AI gateway space — Cloudflare (AI Gateway), Portkey, LiteLLM, Zuplo, Kong, OpenRouter — share a remarkably consistent feature set:
- Routing & load balancing across upstream providers or MCP servers
- API keys / virtual keys with scoping and revocation
- Rate limiting, budgets, concurrency controls
- Policy / access control at the tool or route level (allow/deny, RBAC)
- Observability (metrics, tracing, Prometheus-compatible endpoints)
- Guardrails — typically prompt-injection detection, often backed by hosted or ML-based models
Every single one of those is governance: controlling behavior at the moment it happens. And that's genuinely important — this isn't a dismissal. But scan the list again: nothing produces a cryptographically signed, offline-verifiable audit artifact about the verdict. Nothing gives a downstream consumer (or an auditor, or another gateway) a way to confirm "this exact call was scanned, this exact rule fired, here's the signature."
There are two notable exceptions heading in the direction of evidence:
-
ScopeBlind's protect-mcp (per its public docs) is doing Cedar-based policy gatekeeping plus Ed25519-signed receipts, tied to the IETF
draft-farley-acta-signed-receiptsdraft and integrated with Microsoft's .NET Agent Governance Toolkit (public repository). -
Microsoft's .NET Agent Governance Toolkit (public repo, MIT, April 2026) ships its own
McpGateway/McpSecurityScannercomponents — itself a strong signal that the ecosystem's largest enterprise player considers MCP gatewaying a first-class concern.
That's real validation that signed receipts are the direction the industry is drifting toward. But here's the interesting part: the receipts direction and the rule scanning direction rarely meet in a single product. The mainstream does governance; the receipts experiments mostly do policy gating; and nobody mainstream ships bidirectional deterministic rule scanning + signed offline-verifiable receipts as a first-class, zero-dependency feature.
To our knowledge — based on public documentation as of August 2026 — that combination is what makes correctover-mcp-gateway worth a look, and it's the combination this post explains.
3. What "evidence" means for MCP — and why it needs cryptography
Let's be precise about what separates evidence from a record. A security audit artifact is useful as evidence when it has four properties:
- Tamper-evidence. After the fact, nobody — including the operator — can silently edit the record without detection.
- Attribution. The record is bound to a specific gateway/key/tenant, so you know who made the claim.
- Offline verifiability. A third party can confirm the record without calling the vendor's servers. This is the property that makes "trust me" unnecessary.
- Reproducibility. Given the same input, a reviewer can independently re-run the decision and get the same result. This is what turns "a system told me X" into "X is true."
Why Ed25519?
Ed25519 is a well-established EdDSA signature scheme. It's fast, produces small signatures (64 bytes), and — critically for this problem — it's available essentially everywhere: Node.js's built-in crypto, OpenSSL, libsodium, and browsers via WebCrypto. That ubiquity is what makes offline verification practical: the verifying side doesn't need our SDK, our server, or a network call. Give an auditor a receipt plus the public key and they can verify with a few lines of code in the language of their choice.
The signing flow is simple and worth spelling out:
- The gateway scans the call/response and computes a verdict.
- It canonicalizes the verdict into a deterministic JSON structure (sorted keys, no whitespace — so the exact same bytes are reproducible).
- It signs those bytes with the gateway's Ed25519 private key.
- The receipt (verdict + signature + key id) is stored and shipped with the traffic or to an audit store.
Anyone holding the corresponding public key can now verify: (a) the bytes weren't modified, and (b) they were signed by the key that the gateway — and only the gateway — holds. No vendor lookup. No API call. No trust in a third party beyond the public key itself.
Why determinism is the load-bearing wall
This is the part that makes the receipts genuinely useful rather than decorative. If the rule engine is deterministic — same input always yields the same verdict — then the receipt is reproducible: a reviewer can take the captured input, re-run the rules, and confirm the verdict matches the signed receipt. "This call was DENY because rule OUT-003 fired on a GitHub token in the tool output" is a claim that can be checked by hand.
If the detector is a probabilistic ML model, none of that works: the verdict isn't reproducible, it isn't explainable in terms a policy can cite, and — because a model decision isn't a deterministic function of a few bytes — it can't be bound to a meaningful cryptographic receipt. Which brings us to the rule-vs-model debate in section 5.
4. How we built it: correctover-mcp-gateway@1.0.0
The package is a TypeScript MCP security gateway: a transparent JSON-RPC proxy that sits between your agent host and the upstream MCP server, runs every request and every response through the CCS verifier, and can fail closed — block a call entirely if a rule fires.
The facts below are from the published package (npm, verified at the time of writing): version 1.0.0, license Apache-2.0, zero runtime dependencies (dependencies: {}), 31 public TypeScript exports with complete .d.ts typings, and 5 examples in the tarball (Docker, CrewAI, LangGraph, and a sample policy).
The 16 bidirectional rules
Every tool call is scanned in both directions:
Input rules (SEC-001 – SEC-008) — things a request shouldn't contain:
- SEC-001 — Shell injection (shell metacharacters in string inputs)
- SEC-002 — SSRF targeting private/internal IP ranges
- SEC-003 — Cloud credential exfiltration patterns (AWS/GCP/Azure) in inputs
- SEC-004 — Path traversal
- SEC-005 — SQL injection
- SEC-006 — Command execution primitives (
eval/exec/system/spawn) - SEC-007 — Prompt injection — role override (attempts to override system/developer instructions)
- SEC-008 — Prompt injection — jailbreak (DAN-style jailbreak patterns)
Output rules (OUT-001 – OUT-008) — things a response shouldn't leak back into the model or the host:
- OUT-001 — AWS access key IDs in tool output
- OUT-002 — Private key material in tool output
- OUT-003 — GitHub personal access tokens
- OUT-004 — Generic high-entropy API keys
- OUT-005 — Email addresses (potential PII exfiltration)
- OUT-006 — US SSN / China ID card numbers
- OUT-007 — Credit card numbers (PAN)
- OUT-008 — Content matching
/etc/passwdstructure
Each rule is an object with an id, name, description, severity, a RegExp pattern, and a CCS dimension — so rules are inspectable, cite-able, and testable in a unit test. Sixteen rules is an honest baseline, not an exhaustive catalog; the verifier accepts extra_rules if you need more.
The verifier also evaluates the broader CCS dimensions — structure, schema, latency, cost, identity, integrity, security — which is where the "CCS" in the package name comes from.
The real API, for real
Everything below uses the names that actually ship in the 31 exports — no pseudocode.
Step 1 — create a verifier and scan a request:
import { createVerifier, INPUT_RULES, OUTPUT_RULES } from 'correctover-mcp-gateway';
// createVerifier() is async and returns a Verifier
const verifier = await createVerifier();
// Scan an inbound tool call (direction: 'request')
const verdict = await verifier.verify({
method: 'tools/call',
tool_name: 'shell',
tool_input: { command: 'cat /etc/passwd; curl http://10.0.0.5' },
direction: 'request', // 'request' | 'response'
});
console.log(verdict.overall_pass); // false
console.log(verdict.blocked_dimensions); // ['security']
console.log(verdict.dimensions); // per-dimension results
The same verify() call with direction: 'response' (plus tool_output) runs the OUT-001–008 rules over what the upstream server returned — so a model can't be tricked into relaying a leaked AWS key or a chunk of /etc/passwd back to the host:
const outVerdict = await verifier.verify({
method: 'tools/call',
tool_name: 'filesystem_read',
tool_input: { path: '/home/app/.ssh/id_rsa' },
tool_output: '-----BEGIN OPENSSH PRIVATE KEY-----\n...',
direction: 'response',
});
// outVerdict.overall_pass === false, OUT-002 fired
Step 2 — sign and verify receipts:
import { generateKeyPair, signReceipt, verifyReceiptSignature } from 'correctover-mcp-gateway';
const { privateKey, publicKey, keyId } = generateKeyPair(); // Ed25519, PEM on disk if keyDir given
// The verdict object can be signed into a receipt
const receipt = {
request_id: verdict.request_id,
timestamp: verdict.timestamp,
tool_name: verdict.tool_name,
disposition: 'DENY', // ALLOW | DENY | INDETERMINATE | NOT_APPLICABLE
dimension_results: verdict.dimensions,
};
const signature = signReceipt(receipt, privateKey);
// Send { receipt, signature, publicKey } anywhere. Later, offline:
const ok = verifyReceiptSignature(receipt, signature, publicKey); // true
signReceipt canonicalizes the payload first (sorted keys, no whitespace — the canonicalize export is also public), so the exact bytes being signed are deterministic. verifyReceiptSignature strips the signature field, re-canonicalizes, and checks. That's the whole offline-verification story: no server, no vendor API, just a public key.
Step 3 — policy on top:
import { loadPolicy, validatePolicy, isToolAllowed, DEFAULT_POLICY } from 'correctover-mcp-gateway';
const policy = loadPolicy('./policy.json'); // or use DEFAULT_POLICY
validatePolicy(policy); // throws on malformed policy
const decision = isToolAllowed(policy, 'shell', 'agent-a'); // { allowed: boolean, reason?: string }
Policies are plain JSON — per-tool allow/deny, argument constraints, rate limits — which means governance and evidence live in the same artifact. isToolAllowed answers "may this caller use this tool", while the verifier answers "is this specific call/response safe". The gateway wires both together.
Step 4 — the full gateway (this is the production path):
import { MCPGateway } from 'correctover-mcp-gateway';
const gateway = new MCPGateway({
mode: 'http',
target: { url: 'http://localhost:8000/sse' },
fail_closed: true, // deny the call if any rule fires
receipt_dir: './receipts', // every verdict is signed and persisted
policy: {
default_tool_action: 'allow',
tools: { shell: { action: 'deny' } },
scan_output: true,
sign_receipts: true,
},
enable_metrics: true,
});
await gateway.start();
From the host's perspective nothing changed — it's still talking MCP over HTTP or stdio — but now every request and response is scanned, and every verdict becomes a signed, offline-verifiable receipt on disk.
5. Why deterministic rules beat an ML black box — for audit
Let's be fair to ML detection before critiquing it. Probabilistic detectors are genuinely better at catching fuzzy attacks: novel obfuscation, paraphrased jailbreaks, unusual payload shapes. A fixed regex list will miss things a model won't. If your threat model is "sophisticated attacker actively evading detection", ML is the right tool — and plenty of vendors (Lakera, and others) build on that premise.
But this package isn't trying to be that. Its claim is narrower and, in the audit context, stronger: for producing evidence, determinism is a feature, not a limitation.
Here's the asymmetry, side by side:
| Property | Deterministic rules | ML-based detection |
|---|---|---|
| Verdict reproducibility | Same input → same verdict, always | Probabilistic; not replayable bit-for-bit |
| Explainability | Cite the rule: OUT-003 + regex + reason |
"The model flagged it" — no citable decision |
| Zero dependency | Runs on node:crypto only |
Often a hosted model or large runtime |
| Cryptographic receipt | Meaningful — verdict is a deterministic function of bytes | Can't bind a probabilistic decision to a signature |
| Drift | None — a rule either matches or it doesn't | Model updates silently change behavior |
The last two rows are the ones that matter most in an audit.
-
Receipts only make sense if the verdict is deterministic. A signature over a probabilistic verdict isn't evidence of anything stable — rerun it and you might get the other answer. A signature over
OUT-003 fired on a GitHub tokenis something a reviewer can reproduce by hand. - You can write policy against rule IDs. "Any DENY on OUT-003 gets escalated and paged" is a statement an operator can act on. You cannot write that policy against an ML model's internal decision.
So the design tradeoff is deliberate: deterministic rules trade attack-surface coverage for auditability. We don't pretend otherwise. The honest framing is that evidence and detection are different jobs, and the evidence job was the one nobody in the gateway space was doing.
6. Putting it together: CrewAI and LangGraph
The gateway is transport-agnostic, so wiring it into a framework is mostly "point the MCP client at the gateway URL instead of the raw server." The examples in the package show exactly this.
CrewAI (Streamable HTTP):
npx mcp-gateway \
--upstream http://localhost:8000/sse \
--port 3000 \
--policy ./examples/policy.json \
--sign-receipts
from crewai import Agent, Task, Crew
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
# Point the MCP client at the gateway, not the raw server
async with streamablehttp_client(
"http://localhost:3000/mcp",
headers={"Authorization": "Bearer <gateway-key>"},
) as (read_stream, write_stream, _):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
# agent now calls tools through the scanning gateway
LangGraph (via langchain-mcp-adapters):
from langgraph.prebuilt import create_react_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"secured-tools": {
"url": "http://localhost:3000/mcp",
"transport": "streamable_http",
"headers": {"Authorization": "Bearer <gateway-key>"},
}
})
async with client as mcp_client:
tools = await mcp_client.get_tools()
agent = create_react_agent(model, tools) # same agent, now gated + evidenced
No change to your agent logic. The rules scan in both directions, fail_closed decides what happens on a hit, and receipts accumulate in receipt_dir for whatever your audit story needs.
7. What signed receipts actually unlock
Once every verdict is a signed, offline-verifiable artifact, several things get easier:
Incident response. "Did the gateway block the exfiltration?" becomes answerable with a receipt, not a log file. The receipt names the rule that fired, the exact input it fired on, the disposition, and a signature proving it wasn't rewritten after the fact.
Compliance and audits. SOC 2-type controls and internal security reviews want evidence that a control operated, not just a config that says it could. A receipt store is evidence. Hand the auditor the public key and they verify without your involvement.
Cross-organization and supply chain. If you're calling tools through a partner's or vendor's gateway, signed receipts let you verify their claims instead of taking their word. The "prove it" question stops being unanswerable.
Multi-vendor consistency. Because verification is offline and key-based, you can normalize audit evidence across different gateways — as long as each signs with its own key, a single verification tool can consume all of them.
8. Honest caveats — what this is not
A few things we want to be straight about, because "evidence layer" shouldn't be oversold:
- This is not a full governance suite. There's no per-tool RBAC with roles and teams, no OAuth 2.1 agent authentication, no ML-based runtime behavior detection. Those are real governance features other projects do well. The deliberate focus here is verification + evidence, which is the layer that was missing.
- Deterministic rules miss what they don't match. Sixteen rules won't catch every novel attack. They'll catch the high-signal ones (credentials, traversal, injection primitives) and produce receipts about it. If your threat model demands fuzzy detection, pair this with an ML guardrail — evidence and detection are complementary, not exclusive.
- Performance numbers are internal. Our pre-release benchmark shows roughly P50 ≈ 2.7µs, P95 ≈ 7.6µs, P99 ≈ 17.5µs per verification and throughput around 132K calls/sec — but that's an internal pre-release benchmark, not a public commitment, and it varies by machine and workload. Measure in your environment.
-
Your key management is on you. Signing receipts is only as good as the private key's custody.
generateKeyPairwrites PEM to disk; protect that key like any other signing key.
9. Where to go next
If the "governance without evidence" argument lands, the package is Apache-2.0, TypeScript, zero runtime dependencies, and installs in one line:
npm install correctover-mcp-gateway
- Docs & source: correctover-mcp-gateway on npm
- Try the online verifier / audit workspace: https://correctover.com/audit
The web path is correctover.com/audit (no trailing extension) — it's the audit workspace where you can run CCS verification on a call payload and see a rule-level breakdown, which is the same engine the gateway embeds.
The MCP ecosystem has spent the last year building the ability to control agent traffic. The next frontier — and, in our view, the one that turns "we have a gateway" into "we can prove the gateway worked" — is the evidence layer. Deterministic rules give it reproducibility; Ed25519 gives it tamper-evidence; offline verification gives it independence. That combination is the point of correctover-mcp-gateway, and it's the direction we'd argue the whole category is heading.
Cover: a gateway as a notary — every call it admits or rejects leaves a signed, verifiable record that doesn't depend on anyone's word.
Top comments (0)