DEV Community

Heimdall MCP: Add OpenTelemetry tracing to any MCP server without touching its code

If you've been building with MCP servers lately, you've probably hit this wall: something goes wrong (or just feels slow) and you have zero visibility into what actually happened.

Which tool did Claude call? What input did it send? Did the server error silently? How long did it take?

That's the gap Heimdall fills.


What is Heimdall?

Heimdall is a transparent proxy for MCP servers. It sits between your MCP client (Claude Desktop, OpenCode, Cursor, or any other) and your MCP server — local or remote — and records every interaction as an OpenTelemetry span.

No modifications to your server. No SDK to integrate. No infra to run. Just wrap your existing server and start seeing what's happening inside.

The name comes from Norse mythology: Heimdall is the guardian of the Bifrost bridge, with the ability to see and hear everything that crosses between worlds. That's exactly the role this proxy plays.


The problem in one screenshot

You're running a Claude agent that orchestrates 5+ MCP tools. One of them is consistently slow. Another occasionally returns empty results. You have no way to know which one, when, or why — unless you dig into logs manually.

With Heimdall, every tool call becomes a structured span:

{
  "name": "mcp.tool.call",
  "attributes": {
    "gen_ai.tool.name": "search_documents",
    "mcp.duration_ms": 843,
    "mcp.status": "ok"
  },
  "events": [
    { "name": "request",  "body": { "query": "quarterly report" } },
    { "name": "response", "body": { "results": [...] } }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Stored. Queryable. Yours.


Zero config wrapping via mcp.json

The easiest way to use Heimdall requires zero access to the server's source code. Just install it globally and update your mcp.json:

npm install -g @cardor/heimdall-mcp
Enter fullscreen mode Exit fullscreen mode

Before — your current config:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["my-server.js"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

After — wrapped with Heimdall:

{
  "mcpServers": {
    "my-server": {
      "command": "heimdall",
      "args": [
        "--store", "sqlite://~/.heimdall/traces.db",
        "--",
        "node", "my-server.js"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

That's it. Restart your client and every tool call starts being recorded. The -- separator tells Heimdall where its args end and the real server command begins.

For remote servers (HTTP or SSE), it's just as simple:

{
  "mcpServers": {
    "remote-server": {
      "command": "heimdall",
      "args": [
        "--store", "postgres://user:pass@localhost/mydb",
        "--target", "http://my-remote-mcp.com/sse"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Heimdall exposes stdio to your client, talks HTTP/SSE to the real server, and intercepts everything in between.


What gets recorded

Heimdall captures all standard MCP events:

Event What's recorded
tools/call Tool name, input args, response, duration, status
tools/list Available tools and their schemas
resources/read URI, MIME type, response size
prompts/get Prompt name, arguments, rendered output
initialize Client/server versions, negotiated capabilities
shutdown Total session duration

Every span follows the OpenTelemetry gen_ai.* semantic conventions,
so if you later want to send traces to Jaeger, Honeycomb, or Grafana Tempo, the data is already well-formed.


Storage options

Pick the backend that fits your setup:

SQLite — local file, zero infra

--store sqlite://./traces.db
# or with absolute path
--store sqlite:///Users/you/.heimdall/traces.db
Enter fullscreen mode Exit fullscreen mode

Best for: local development, single-machine setups, quick debugging sessions.

Uses @libsql/client under the hood — pure WASM, no native bindings, no node-gyp.

Postgres — your existing database

--store postgres://user:password@localhost:5432/mydb
Enter fullscreen mode Exit fullscreen mode

Best for: production environments, teams sharing observability data,
long-term storage with SQL queries across multiple servers.

MySQL

--store mysql://user:password@localhost:3306/mydb
Enter fullscreen mode Exit fullscreen mode

Same use case as Postgres. Pick whichever you already run.

All three use drizzle-orm with a shared schema, so the query experience is consistent regardless of which backend you choose.


Using it as a TypeScript library

If you have access to your server's code and want tighter integration, Heimdall ships as a fully-typed TypeScript library with a fluent builder API:

import { McpProxy } from 'heimdall-mcp'

const proxy = await McpProxy
  .create()
  .inbound({ transport: 'stdio' })
  .outbound({ transport: 'http', url: 'http://localhost:3001' })
  .store('sqlite://./traces.db')
  .build()

await proxy.start()
Enter fullscreen mode Exit fullscreen mode

You can also plug in custom interceptors — for example, to redact sensitive fields before they're persisted, or to add your own business logic on top of the tracing:

import { McpProxy, type Interceptor } from 'heimdall-mcp'

// Custom interceptor: redact API keys from tool inputs before storing
const redactSecrets: Interceptor = {
  name: 'redact-secrets',
  async intercept(request, ctx, next) {
    const sanitized = redactSensitiveFields(request)
    return next(sanitized)
  }
}

const proxy = await McpProxy
  .create()
  .inbound({ transport: 'stdio' })
  .outbound({ transport: 'http', url: 'http://localhost:3001' })
  .store('postgres://user:pass@host/db')
  .intercept(redactSecrets)
  .build()
Enter fullscreen mode Exit fullscreen mode

Design goals

A few decisions worth calling out:

No native dependencies. SQLite runs via WASM (@libsql/client), Postgres via the pure-JS postgres package, MySQL via mysql2. You can install and run Heimdall in any Node 22+ environment without compilation steps.

No policy opinions. Heimdall doesn't block, approve, or modify tool calls. It only observes and records. If you need access control or rate limiting, pair it with a tool like mcpwall.

Transport mixing. Your client connects via stdio. Your server can be stdio, HTTP, or SSE. Heimdall bridges them transparently — useful for wrapping local CLI servers behind an HTTP interface without touching their code.


Get started

npm install -g @cardor/heimdall-mcp
Enter fullscreen mode Exit fullscreen mode

GitHub: github.com/enmanuelmag/heimdall-mcp
Website: https://heimdall-mcp.cardor.dev

If you try it out, I'd love to hear what you think — especially feedback on the interceptor API design and the OTel schema. Open an issue or find me on X [@enmanuelmag].

Top comments (0)