How it works

A reference monitor at the protocol boundary

euno is a transparent MCP proxy that speaks the protocol on both ends and evaluates capability policy in the middle. The decision happens before the upstream is contacted, and every event is signed and audited.

On this page

Architecture

The proxy sits between any MCP host (Claude Desktop, Cursor, Windsurf, LangChain.js, …) and any upstream MCP server. It is transparent on the protocol — the host sees an MCP server, the upstream sees an MCP client.

flowchart TD MCP_Host("MCP host
(Claude / Cursor / Windsurf / agent)") Upstream_Server("Upstream MCP server
(filesystem, postgres, github, slack, fetch, your own…)") subgraph Euno_MCP["@euno/mcp"] direction TB PolicyEngine("Policy engine
evaluate
conditions") AuditSink("Audit sink
(OCSF + HMAC)
~/.euno/audit.jsonl") Proxy("StdioProxy / HTTP transport") PolicyEngine -- "allow" --> Proxy end MCP_Host -- "tools/call" --> PolicyEngine PolicyEngine -- "deny" --> MCP_Host PolicyEngine --> AuditSink Proxy -- "forward call" --> Upstream_Server Upstream_Server -. "response" .-> Proxy Proxy -. "response" .-> PolicyEngine PolicyEngine -. "post-process" .-> MCP_Host

Two transports are supported: stdio (Claude / Cursor / Windsurf) and HTTP (LangChain.js and other in-process clients). The same policy file works for both. ipRange is enforceable only on HTTP — stdio sessions have no source IP.

Request flow

  1. The host issues a tools/call over MCP.
  2. The proxy parses the call, looks up the matching capability in the policy, and evaluates each condition in order. The first deny wins.
  3. If allowed, the call is forwarded to the upstream over the same MCP transport. The upstream's response is post-processed (e.g. redactFields) and returned to the host.
  4. If denied, the upstream is never contacted. The host receives a structured CapabilityDenied with the errorCode, conditionType, and statusCode.
  5. Every decision — allow or deny — is written to the OCSF audit log, signed with HMAC-SHA256.
flowchart TD Host[Host] Proxy[proxy] Parse[parse] Match[match capability] Eval{evaluate conditions} %% Main Request Flow Host -->|tools/call| Proxy Proxy --> Parse Parse --> Match Match --> Eval %% Execution and Processing Nodes Upstream[forward to upstream] Post[post-process] Audit[("OCSF audit
~/.euno/audit.jsonl")] %% Allow Path Eval -->|allow| Upstream Upstream -.->|response| Post %% Return Paths back to Host Post -.->|result| Host Eval -->|deny| Host %% Audit Log Paths Post --> Audit Eval -->|deny| Audit

Schema parity — one shape, zero drift

The policy file is a literal subset of AgentCapabilityManifest from @euno/common-core (Apache-2.0). @euno/mcp imports condition types directly from @euno/common-core and never defines its own. This means:

Audit log internals

Each event conforms to the OCSF API Activity class (class_uid: 6003). The signer:

The log rotates at 100 MiB. Rotated files are named audit.jsonl.<ISO-timestamp>. verifyAuditEvent() recomputes the HMAC for round-trip integrity checks, and euno-mcp stats aggregates denials across active and rotated logs into an ASCII histogram grouped by conditionType and denialCode.

Why HMAC and not a digital signature? The local audit log is tamper-evident — anyone with read access to the key can verify, anyone without it cannot forge.

Enforcement guarantees

Enforcement runs on the arguments the agent actually sent — before the upstream is called. The guarantee is: "the agent sent this tool call with these arguments, and it was allowed/denied by this policy." It is not a guarantee about what the upstream server does internally — for that, instrument the upstream as well.

Two practical caveats:

Current scope

euno is a local-first policy layer for MCP and LangChain.js integrations. The current release ships the MCP proxy, the in-process LangChain.js runtime, signed OCSF audit logging, and reference policies for common agent backends.

For operational details, see the deployment guide, the repository guide, and the package READMEs.

Try it in 60 seconds →