Heimdall MCP

Heimdall MCP

posted 4 min read

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": [...] } }
  ]
}

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

Before — your current config:

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

After — wrapped with Heimdall:

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

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"
      ]
    }
  }
}

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

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

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

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()

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()

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.

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

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

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].

More Posts

MCP-Airflow-API

JungJungIn - May 2

MCP-Ambari-API

JungJungIn - May 2

MCP-PostgreSQL-Ops

JungJungIn - May 1

MCP-OpenStack-Ops

JungJungIn - May 1

How to Configure Nginx as a Proxy Server?

Ganesh Kumar - May 1
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

1 comment
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!