On this page
- Prerequisites
- Install
- Wiretap (zero-config)
- Author a policy
- Validate the policy
- Run the proxy
- Try it from a host
- Inspect the audit log
- Claude Desktop / Cursor / Windsurf
- Desktop Extension (.mcpb)
- HTTP transport · any MCP client
- Gateway · many upstreams
- Redis · shared state
- Go SDK embedding
- Next steps
Prerequisites
-
The
eunox-mcpbinary on yourPATH— install it via Homebrew, winget, Docker, a native package, orgo install(step 1). No Go toolchain is required unless you build from source. - An MCP host: Claude Desktop, Cursor, Windsurf, or any agent that speaks MCP
-
An upstream MCP server you want to govern (for example
@modelcontextprotocol/server-filesystem)
eunox-mcp and its supporting Go
packages (pkg/capability, pkg/enforcement,
pkg/callcounter, pkg/killswitch, and more) are
Apache-2.0 — free to use, embed, and redistribute. See
LICENSE
for details.
1. Install
Install the eunox-mcp binary with your platform's package
manager, pull the container image, or grab a pre-built binary. Every
release is signed with
Sigstore
keyless signing and ships with an SPDX SBOM per artifact — see
Verifying a release
for the cosign commands.
macOS / Linux — Homebrew
brew install eunolabs/tap/eunox-mcp
Windows — winget
winget install eunolabs.eunox-mcp
Docker
docker pull ghcr.io/eunolabs/eunox-mcp:latest
Debian / Ubuntu (.deb) · Fedora / RHEL (.rpm) ·
pre-built binaries
Native packages and pre-built binaries for every supported platform are
attached to each
GitHub release. Pick the asset that matches your OS and architecture and install with
dpkg -i, rpm -i, or unpack the tarball onto
your PATH:
# macOS / Linux — replace OS and ARCH as needed (darwin/linux, amd64/arm64)
curl -sSL https://github.com/eunolabs/eunox/releases/latest/download/eunox-mcp_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz \
| tar -xz eunox-mcp
chmod +x eunox-mcp
sudo mv eunox-mcp /usr/local/bin/
From source (Go 1.25+)
go install github.com/eunolabs/eunox/cmd/eunox-mcp@latest
However you installed it, confirm the binary is on your
PATH:
eunox-mcp version
Wiretap — zero-config observation
Before writing a policy, see what your agent actually calls. Front any
MCP server with --audit — no config file, no manifest.
Every call is forwarded, every call is recorded to
~/.eunox/audit.jsonl with full arguments and an HMAC
signature.
# wrap any stdio subprocess MCP server
eunox-mcp proxy --audit -- npx -y @modelcontextprotocol/server-filesystem /data
# or a remote MCP HTTP server
eunox-mcp proxy --audit --upstream-url https://mcp.example.com
# point your MCP host at the command above, use the agent for a while, then:
eunox-mcp stats
eunox-mcp stats prints a per-tool histogram split into
BLOCKED (none — wiretap blocks nothing) and
OBSERVED (would-be denials if you had a policy), so
the call patterns and argument shapes guide your first manifest. When
you're ready to enforce, step 2 turns that tape straight into a draft
policy.
2. Author a policy
A policy is a single YAML file describing which tools are allowed and
the conditions on each call. You don't have to write it from a blank
page — eunox-mcp suggest reads the wiretap tape and drafts
one for you, grounded in what the agent actually did:
eunox-mcp suggest --output eunox.policy.yaml
It emits one entry per observed target and, for tool arguments seen with
a bounded set of string values, an allowedValues condition
listing those values (with a commented glob hint when they share a
directory prefix). The sensitive sampling opt-in is always left
commented out, and anything seen only as a denial is listed commented so
regenerating never silently widens access. The output is a
draft describing observed usage, not vetted policy —
review and tighten every entry before enforcing.
A hand-written policy has the same shape. Save the following as
eunox.policy.yaml to see the anatomy:
schemaVersion: "0.1"
name: filesystem-agent
version: 0.1.0
capabilities:
- target: tool:read_file # "tool:" prefix required
actions: [call]
conditions:
- type: allowedExtensions
argument: path
extensions: [".csv", ".json", ".txt", ".md"]
- type: maxCalls
count: 50
windowSeconds: 60
- target: tool:list_directory
actions: [call]
The manifest format follows the
MCP Capability Manifest Specification. The Go implementation tracks the spec exactly — there is one shape
shared across pkg/capability, pkg/enforcement,
and eunox-mcp with no drift.
3. Validate the policy
eunox-mcp validate ./eunox.policy.yaml
Validation runs without contacting any server and rejects unknown condition types and malformed YAML up front.
4. Run the proxy
Declare the upstream in an eunox.yaml and point the proxy at
it:
schemaVersion: "0.1"
transport: stdio
upstreams:
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
policy: ["./eunox.policy.yaml"]
eunox-mcp proxy --config ./eunox.yaml
The proxy speaks MCP on stdio in both directions and is transparent to
clients. (eunox-mcp init --upstream-url <url> --output
eunox.policy.yaml --config-output eunox.yaml scaffolds both files
from a live server.)
5. Try it from a host
Point your MCP host (or any client) at the proxy command above. From
Claude Desktop, attempt to read
/data/keys.pem — the proxy denies it before the upstream is
contacted, and the agent receives a structured
CapabilityDenied error.
6. Inspect the audit log
Every decision — allow or deny — is recorded as a signed OCSF API Activity event:
tail -F ~/.eunox/audit.jsonl | jq
For aggregated denial counts, run:
eunox-mcp stats
This prints an ASCII histogram grouped by condition type and denial code, including rotated log files.
Claude Desktop / Cursor / Windsurf
Add the proxy as an mcpServers entry in
claude_desktop_config.json (or the equivalent file for
Cursor / Windsurf):
{
"mcpServers": {
"filesystem-governed": {
"command": "eunox-mcp",
"args": ["proxy", "--config", "/absolute/path/to/eunox.yaml"]
}
}
}
Omit a route's policy: (or set enforcement: audit)
— handy for capturing an audit trail before you write any rules.
Desktop Extension (.mcpb) — one-click install
Claude Desktop can also install eunox as a signed
Desktop Extension: a .mcpb bundle that
ships the static binary and registers it through the UI, with no
claude_desktop_config.json editing. eunox is one static
binary, so it packs as a binary-type bundle (one per
platform).
-
Download the bundle for your platform —
eunox-mcp_<version>_<os>_<arch>.mcpb— from the latest release. Optionally verify it like any other release artifact: the.mcpbhashes live in the release's singlechecksums.txt(covered by one Sigstore signature), and each bundle ships an SPDX SBOM and a SLSA build-provenance attestation. -
Install it in Claude Desktop under
Settings → Extensions → Advanced settings → Install
Extension…
and select the
.mcpb. -
Configure the eunox config (eunox.yaml)
prompt with the absolute path to your
eunox.yaml, not the.mcpbbundle you just downloaded. No config yet? Scaffold one from a live server witheunox-mcp init --upstream-url <url> --output eunox.policy.yaml --config-output eunox.yaml, or copy the observe-only starter atexamples/eunox.example.yaml.
.mcpb here. A
.mcpb is a binary archive, not a config. Pointing the config
field at it makes eunox fail closed at startup (it reports the file "looks
like a ZIP archive … not a text config"), so the extension serves no
tools — pick your eunox.yaml instead.
The extension launches eunox in enforce mode
(proxy --config <your eunox.yaml>), so the tool list
Claude sees is already filtered by your manifest. A
transport: stdio upstream is launched as a subprocess, so
its runtime (npx / uvx / python)
must be reachable on Claude Desktop's PATH — use an
absolute command path, or a transport: http upstream, if it
fails to start. The full per-client walkthrough lives in the
client-integration guide.
HTTP transport · any MCP client
For agents that connect over HTTP rather than stdio, set the host
transport to http in the config:
schemaVersion: "0.1"
transport: http
listen: { bind: 127.0.0.1, port: 7391 }
upstreams:
- name: local
transport: stdio
command: ./my-mcp-server
policy: ["./eunox.policy.yaml"]
eunox-mcp proxy --config ./eunox.yaml
Connect your client to http://127.0.0.1:7391/mcp/local. The
same policy file works on both transports — and
ipRange conditions become enforceable here.
Gateway · many upstreams, one process
The recommended HTTP shape is the gateway: one process
fronting every MCP server you use, each on its own
/mcp/<name> route with its own versioned manifest, all
sharing one signed audit tape. Declare the upstreams in a config:
schemaVersion: "0.1"
upstreams:
- name: filesystem # → /mcp/filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
policy: ["./policies/filesystem.yaml"]
expectVersion: "0.1.0" # fail closed if the manifest version differs
- name: stripe # → /mcp/stripe
transport: http
upstreamUrl: https://mcp.stripe.com
policy: ["./policies/stripe.yaml"]
eunox-mcp proxy --config gateway.yaml
Every audit record is stamped with the upstream,
policy_version, and a policy_sha256 digest. A
single remote upstream is just a gateway with one route.
Two things worth knowing before you copy this: expectVersion
pins a single manifest file (merge a multi-file policy
first), and a route with no policy: at all allows every call
— the gateway warns about that at startup. Put secrets in the environment
and reference them with ${VAR}; a literal $ in a
token is preserved, not eaten by expansion.
Redis — shared state across instances
The maxCalls rate limit in the policy above keeps its
counters in-process by default — one set per proxy, reset when the proxy
restarts. The same is true of the kill switch. That is exactly right for
a single proxy, a developer laptop, or CI, and needs no setup. But run
more than one proxy instance behind a load balancer, or
require counters and revocations to survive a restart, and that
per-process state has to live somewhere shared. Point the proxy at
Redis:
eunox-mcp proxy \
--config ./eunox.yaml \
--redis-addr localhost:6379 # add --redis-password / --redis-tls if your Redis needs them
With --redis-addr set, every instance reads and writes the
same call-counter and kill-switch state: a maxCalls quota is
enforced across the whole fleet, and an eunox-mcp kill
revocation reaches every instance. Nothing else changes — same policy,
same audit tape, same transports. Redis is the proxy's only optional
runtime dependency; leave it off and everything still works, just
per-process. (A policy that uses maxCalls without
--redis-addr prints a startup notice to that effect.)
For the production trade-offs — how the kill switch fails
closed during a Redis outage, and
--killswitch-fail-open if you would rather keep the data
plane available — see the
deployment guide.
Go packages — in-process embedding
If your agent runtime already runs in Go and you do not want a separate proxy process, reuse the same manifest and enforcement primitives directly:
-
pkg/capability— manifest and condition types (Apache-2.0). -
pkg/enforcement— enforcement engine and decision flow (Apache-2.0). -
pkg/callcounter— per-session rate-limit counters (Apache-2.0).
Same YAML policy model, same conditions, same audit semantics — evaluate a manifest in-process without any proxy hop.
Next steps
- Drop in a ready-made policy from the reference policies page.
- Browse the full condition matrix with worked demos for each.
- Read how the proxy enforces policy end-to-end.
- Explore the source on GitHub — issues and PRs welcome.