Anything that logs every tool call is one config mistake away from writing secrets to disk, so `--body-mode redacted` — sizes only — being the default matters more than it sounds; `hash` gives you comparability without contents, and `full` is labelled local and development only. The other reason to reach for this is the merge rule: a global config sets a floor a project cannot lower, so a team-wide deny stays denied no matter what lands in someone's repo.
A proxy that sits between an MCP client and a real server, intercepting every JSON-RPC message. It exposes stdio to the client and speaks stdio, HTTP or SSE to the server behind it, without changing that server's code.
- Policy at the proxy layer: a `heimdall.config.ts` names, per server, which tools, prompts and resources may reach the agent, with allow and deny lists
- `toolPolicies` adds argument-level constraints on top of name matching, including path scoping and a `warn_only` mode
- Local and global configs merge security-first: denies union, allows intersect, and a global rule cannot be loosened locally
- Resource locks with write mode, TTL and read mode, so two agents do not act on the same resource at once
- Every request and response becomes a span with timing, attributes and body, stored in SQLite, PostgreSQL or MySQL
- `--otlp` additionally exports spans to an OTLP HTTP endpoint such as Jaeger or Tempo, while still saving them to the store
- Host policies plus a Claude Code `PreToolUse` hook and an OpenCode plugin let lock enforcement reach the client side
`npm install -g @cardor/heimdall-mcp`, then wrap the real server in your client config: `heimdall-mcp --store sqlite://~/.mcp-traces/traces.db -- npx @modelcontextprotocol/server-filesystem /tmp`. `--store` is required. `--out http` or `--out sse` with `--target` wraps an already-running remote server instead of spawning one. `--body-mode` defaults to `redacted`, with `hash` and `full` as the other options.
One command — npm install -g @cardor/heimdall-mcp
