Quick start

Ship a governed agent in 60 seconds

Start with one line — no config — to observe what your agent calls. Then graduate to a YAML policy when you're ready to enforce. The Go-based proxy runs locally, wraps any MCP server, and writes a tamper-evident audit tape on the way.

On this page

Prerequisites

License. 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).

  1. 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 .mcpb hashes live in the release's single checksums.txt (covered by one Sigstore signature), and each bundle ships an SPDX SBOM and a SLSA build-provenance attestation.
  2. Install it in Claude Desktop under Settings → Extensions → Advanced settings → Install Extension… and select the .mcpb.
  3. Configure the eunox config (eunox.yaml) prompt with the absolute path to your eunox.yaml, not the .mcpb bundle you just downloaded. No config yet? Scaffold one from a live server with eunox-mcp init --upstream-url <url> --output eunox.policy.yaml --config-output eunox.yaml, or copy the observe-only starter at examples/eunox.example.yaml.
Don't select the .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:

Same YAML policy model, same conditions, same audit semantics — evaluate a manifest in-process without any proxy hop.

Next steps