# MCP server

**Every MCP client that connects to a server it downloaded from npm is trusting a stranger with tool-calling access to whatever the model can reach.**

Most servers give it no way to check who it's actually talking to — or to prove who *it* is when another agent connects.

The Model Context Protocol standardized *how* a client and a server talk (JSON-RPC 2.0 over stdio, `tools/list`, `tools/call`), but it left *trust* as someone else's problem. A server can claim to be "the finance-approval tool" in its `tools/list` description; nothing stops it from lying, and nothing lets the calling model check. Whisper ships one MCP server, `io.github.whisper-sec/whisper`, that closes exactly that gap: two keyless tools any MCP client can call to verify a real network identity against the DNSSEC root — no account, no key, no trust in Whisper's word required — plus, when your API key is present, the same control-plane verbs the [CLI](/docs/cli) and [SDKs](/docs/sdk-python) expose, as tools an LLM can call directly: register an agent, read logs, set policy, revoke, get egress config. One binary, one registry entry, stdio in, JSON out.

## Why MCP servers need an identity primitive, not just a directory listing

- **`tools/list` is self-asserted.** A server's `name` and `description` fields are exactly as trustworthy as a bearer token pasted into a prompt — the server wrote them itself. There is no field in the MCP spec that lets a client cryptographically confirm *which* server, or which agent behind it, it's actually connected to.
- **Registries index metadata, not identity.** The [MCP Registry](https://github.com/modelcontextprotocol/registry) (`server.json`, published via GitHub Actions OIDC) proves a server's *publisher* controls the named GitHub repo — a supply-chain fact. It says nothing about the *runtime* identity of whichever process ends up handling a given tool call, especially once a server proxies to other agents.
- **The fix is the same one two agents use to trust each other.** Whisper's answer here is exactly the [agent-to-agent trust](/docs/a2a-trust) model applied to MCP: give every agent a real, routable IPv6 `/128` whose forward DNS, reverse DNS, DANE `TLSA`, and RDAP registration all agree — checkable by anyone, with tools that predate MCP by decades. The `whisper_verify` tool just puts that check one tool-call away for any model that can reach an MCP server.

> Put `whisper_verify` in front of any inbound MCP tool call you don't already trust — a marketplace router dispatching to third-party servers, an orchestrator accepting sub-agent results, a server itself checking who's calling it. One round trip turns "trust the description field" into a fact the model can re-derive.

## The tool set: two keyless, six key-gated

`whisper mcp` starts a stdio JSON-RPC server implementing the core [MCP](https://modelcontextprotocol.io/specification) methods (`initialize`, `tools/list`, `tools/call`, `ping`) — the set that's stable across every spec revision from `2024-11-05` on. It echoes back whatever `protocolVersion` the client sends at `initialize`, so any conformant client (older or newer) negotiates cleanly. Its tool catalogue is two-tier by construction, per the same [Postel's-Law design](/docs/integrations) every Whisper integration follows:

| Tool | Gate | Does |
|---|---|---|
| `whisper_verify` | keyless | Full trust chain (reverse-DNS + DANE-EE TLSA + DNSSEC + JWS) for an address or FQDN → verdict JSON |
| `whisper_rdap` | keyless | RDAP registration record for a `/128` — operator, tenant, issued-since |
| `whisper_register` | key | Create an agent: name in, routable `/128` + DNS name out |
| `whisper_list` | key | List your tenant's agents, DNS records, or identities |
| `whisper_policy` | key | Read or set your tenant's resolver policy (block/allow/default) |
| `whisper_logs` | key | Query an agent's DNS/connection/allocation history |
| `whisper_revoke` | key | Irreversibly withdraw an agent's `/128`, reverse-DNS, and keys |
| `whisper_egress_config` | key | Return the proxy env + `whisper connect`/`run` command to source a workload from an agent's `/128` |

The server decides which half to advertise by resolving the standard key ladder (`WHISPER_API_KEY`/`WHISPER_KEY` env, or a saved `whisper login` credential) once at startup: no credential resolves → `tools/list` returns only the two keyless tools; a credential resolves → all eight. A client that calls a control tool without a key gets a normal MCP tool error (`isError: true`) naming the exact fix, never a silent 500 — the model can read it and act.

## Add it to a client

**With stock tools** — MCP has no special transport magic; it's newline-delimited JSON-RPC over a pipe, so you can drive it with nothing but a shell:

```bash
# start the server and hand it a raw initialize + tools/list, by hand
{
  echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'
  echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
} | whisper mcp | jq -c '.result.tools[]?.name // .result.serverInfo'

# call the keyless verify tool directly, no client needed
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whisper_verify","arguments":{"target":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"}}}' \
  | whisper mcp | jq -r '.result.content[0].text' | jq '{is_whisper_agent, dane_ok}'
```

Wiring a real client is just hand-editing its config file — every MCP client uses the same `{"command", "args"}` shape, only the top-level key and path differ:

```jsonc
// ~/.cursor/mcp.json, ~/.codeium/windsurf/mcp_config.json, or Zed's settings.json
// (Zed's key is "context_servers"; VS Code's .vscode/mcp.json uses "servers" + "type":"stdio")
{
  "mcpServers": {
    "whisper": { "command": "whisper", "args": ["mcp"] }
  }
}
```

**With Whisper**, `whisper mcp install` writes the project-level configs for you (`.mcp.json` for Claude Code, `.cursor/mcp.json` for Cursor) without clobbering any server already there, and prints the exact snippet + file path for every client that only supports a global config:

```bash
whisper init claude          # zero-config: identity + MCP wiring in one call (see /docs/cli)
# — or, in an existing project —
whisper mcp install
#   wrote .mcp.json               (Claude Code)
#   wrote .cursor/mcp.json        (Cursor)
#
#   other clients — paste the snippet into the file shown:
#     Windsurf   ~/.codeium/windsurf/mcp_config.json   "mcpServers": { "whisper": {...} }
#     Claude Desktop (mac)  ~/Library/Application Support/Claude/claude_desktop_config.json
#     VS Code (project)     .vscode/mcp.json   (key is "servers", not mcpServers)
#     Zed        ~/.config/zed/settings.json   (key "context_servers")
#     Goose / Continue      ~/.config/goose/config.yaml / ~/.continue/config.yaml (YAML)

export WHISPER_API_KEY=whisper_live_…   # set in the CLIENT's launch env to unlock the 6 control tools
```

Restart the client and ask it, in chat, to run `whisper_verify` against any address — that's the smoke test; a true/false `is_whisper_agent` back means the wiring is correct end to end.

## Registry, packaging, and the wire protocol underneath

The server ships as a single mode of the same cross-compiled `whisper` binary documented on the [CLI page](/docs/cli) — no separate install, no Python/Node runtime to manage. It's published two ways:

- **Container image** — `ghcr.io/whisper-sec/whisper` (multi-arch, entrypoint `whisper`), the transport the [MCP Registry](https://registry.modelcontextprotocol.io) entry points at: `docker run -i ghcr.io/whisper-sec/whisper mcp`.
- **MCP Registry entry** — `io.github.whisper-sec/whisper`, a `server.json` in the public [`whisper-sec/whisper-cli`](https://github.com/whisper-sec/whisper-cli) repo, publishing via the registry's GitHub Actions OIDC flow (no separate namespace credential — ownership of the repo *is* the proof, the same supply-chain-attestation model the registry uses for every publisher).
- **Native binary** — anywhere `whisper` is already on `PATH` (Homebrew, Scoop, `apt`/`dnf`/`apk` via `get.whisper.online`, `go install`), `whisper mcp` runs the identical server with zero container overhead.

Underneath, every call is plain [JSON-RPC 2.0](https://www.jsonrpc.org/specification) over stdio: a request has `{jsonrpc, id, method, params}`, a notification omits `id` and gets no reply, and a tool failure is a normal `tools/call` *result* with `isError: true` and a human-readable `content[0].text` — never a transport-level error — so a model reading the response can see exactly what went wrong and retry sanely. That's the same clear-error-over-opaque-failure posture as the rest of Whisper's public surface: liberal in what the server accepts (any client, any recent protocol version it negotiates down to), conservative in what it emits (a typed, predictable result every time).

For the broader integration philosophy this server follows — keyless value with zero setup, full control behind a key, proven end-to-end rather than demoed — see [Integrations](/docs/integrations).

---

**Next:** [Agent-to-agent trust](/docs/a2a-trust) — the identity chain `whisper_verify` runs · [CLI](/docs/cli) — the `whisper` binary this server ships inside.
