9 KiB
ADR-0044 — AI agent integration via Model Context Protocol
Status: Accepted, 2026-05-26 Supersedes: none Superseded by: none
Context
Users increasingly ask AI agents ("Claude, ChatGPT, Grok, Perplexity, Cursor, Cline, my local Llama") for help with crypto-related tasks. When a user prompts "I want to buy some Monero", the agent either has to: (a) recommend a custodial KYC'd exchange (the default in current training data, since LocalMonero shut down), (b) recommend a non-custodial alternative it has training-data presence of (Bisq, RoboSats, Haveno), or (c) refuse / be unhelpful.
Morphit ought to be a fourth option. But "ought to be" isn't enough — the agent needs to actually reach Morphit's orderbook to give a useful answer, and surface a path the user can follow to complete the trade.
Two distinct problems:
- Recommendation surface — how AI agents come to know Morphit exists and when to recommend it (SEO, kycnot.me listing, llms.txt, comparison content). This is non-technical and tracked separately.
- Tool surface — how AI agents can actually call into Morphit to answer concrete user questions. This ADR.
Decision
Ship a standalone, read-only Model Context Protocol (MCP) server, distributed as morphit-mcp on npm + Docker, exposing five tools that wrap the existing public /v1/ HTTP API surface.
MCP is the open standard for AI-agent ↔ external-system integration. Announced by Anthropic in late 2024, since adopted by OpenAI, Google, and the open-source AI ecosystem. Picking MCP over a proprietary plugin format (OpenAI Actions, GPT Store custom, Grok plugins, etc.) means one shipped binary covers every MCP-compatible agent — present and future, commercial and self-hosted.
Tools exposed:
morphit_search_orders— orderbook query mirroring/v1/orderbookmorphit_get_listing— single-listing detailmorphit_list_instances— federation directorymorphit_list_payment_methods— per-instance payment-method registrymorphit_describe— structured "what is Morphit" summary
Architectural posture: read-only, deeplink-handoff
The server is deliberately read-only. No tool signs, broadcasts, mutates, or holds keys. When the user wants to act on a listing the AI surfaced, the server returns a deeplink field pointing at the Morphit web UI for the actual key-signing step.
This preserves Morphit's two non-negotiables:
- Non-custodial. Private keys never leave the user's browser. An AI tool that signed on the user's behalf would either need to hold keys (breaks non-custodial) or proxy to a key-holding service (introduces a new custodial intermediary). Read-only sidesteps both.
- Zero-KYC. No accounts, no API keys, no credentials. The MCP server runs on the user's own machine (or wherever they want), queries any Morphit instance over plain HTTP, and identifies itself only via a
User-Agent: morphit-mcp/...header.
The deeplink-handoff pattern also has a practical benefit: the user sees the actual listing on the actual Morphit instance UI before committing, with the full audit trail (chain pin, profile reputation, payment-method terms) the AI summary necessarily compressed.
Federation surface
Single configuration env var: MORPHIT_MCP_INSTANCE_URL (default https://morphit.io). The user (or their MCP client config) picks which instance to query. The morphit_list_instances tool surfaces alternatives so the AI can route to the user's preferred operator — e.g. a Tor-hosted instance for privacy, a regional one for jurisdictional fit.
Every Morphit instance exposes the same /v1/ surface, so the MCP server is instance-agnostic. Operators get the AI-discoverable surface for free by standing up the indexer; no operator-side work required.
Future work — now shipped (beta16); see the addendum below. The stdio-only npm package was the v1; a hardened HTTP transport in the MCP server itself (not the relay — keeping it isolated and key-free) is the v2, making https://morphit.io/mcp a real, reachable endpoint.
Alternatives considered
-
OpenAI Actions / GPT Store custom GPT. Single-vendor lock-in. Doesn't reach Claude, Grok, Perplexity, local LLMs. Could ship in addition to MCP, but not instead of.
-
REST API + AI-agent-side custom integration. That's what Morphit already has (
/v1/openapi.json). MCP layers ergonomic tool-discovery on top of the same surface — the SDK auto-emits a tools/list response from JSON Schema, so the AI agent's tool-selection logic gets clean type information without each agent reimplementing OpenAPI parsing. -
Write/sign-capable MCP tools. Considered and rejected. See "Architectural posture" above. Could revisit if a future MCP profile standardizes user-confirmation-required write actions with key escrow on the user's machine, but the threat model around an AI process holding key material is genuinely difficult and not worth the surface-area gamble for the first cut.
Consequences
Positive.
- Zero per-agent integration work. One binary, every MCP-compatible client.
- Aligns with Morphit's federation model — operators benefit transitively.
- Read-only by architecture preserves both non-custodial and zero-KYC postures.
- Smoke battery extends: 8 mcp-server-smoke scenarios cover wire-protocol, schema advertisement, error paths, and deeplink shape.
Negative.
- Maintains a small additional dependency surface (
@modelcontextprotocol/sdk). Mitigated by SDK being Anthropic-maintained and load-bearing in their own stack. - Requires keeping the JSON-Schema description fields aligned with what AI agents respond best to. This is a quality-of-prompt-engineering concern, not a correctness concern — wrong descriptions cause the agent to pick the wrong tool; they don't break anything.
Source
apps/mcp-server/— full workspaceapps/mcp-server/README.md— Claude Desktop / Cline / Cursor / Continue / Windsurf / Zed integrationapps/mcp-server/scripts/mcp-server-smoke.ts— 8-scenario wire-protocol test- Brag list #99
Addendum (beta16) — HTTP transport shipped
The "future work" HTTP transport is now implemented in the MCP server
itself (not the relay — keeping the MCP isolated and key-free). A single
env var, MORPHIT_MCP_TRANSPORT, selects it: stdio (default) for local
agents that spawn the server as a subprocess; http (the mode
morphit-mcp.service runs) for a hardened, network-reachable endpoint a
reverse proxy can expose for federation-wide remote-agent discovery.
Why it was needed. cp251 shipped a persistent morphit-mcp.service
whose unit, docs, and brag claim all assumed a network HTTP MCP on
127.0.0.1:8124 — but the server was stdio-only. Run as a daemon it read
EOF on its empty stdin and exited 0 in under a second, so nothing ever
listened on 8124 and the advertised /v1/instance.mcp_url (<origin>/mcp)
pointed at a dead upstream.
Transport choice: stateless, JSON-response. sessionIdGenerator is
left undefined with enableJsonResponse: true, so each POST is an
independent request/response — no session table to exhaust, no long-lived
SSE. initialize, tools/list, and tools/call all work per request
(verified end-to-end). The read-only tool set emits no server-initiated
messages, so nothing is lost by dropping the SSE channel. A hand-rolled
JSON-RPC endpoint was rejected (not MCP-spec compliant — standard clients
couldn't connect); stateful Mcp-Session-Id sessions were rejected for
the first cut (a bounded-but-real exhaustion surface for zero benefit
here).
Security posture (the MCP is the most exposed surface). Defense in
depth, all on by default, tunable via MORPHIT_MCP_* in
/etc/morphit/mcp.env: a fail-closed bind that accepts loopback or any
private/bridge address (e.g. 172.18.0.1 for a dockerized reverse proxy,
the same way the indexer/relay are reached) but refuses 0.0.0.0/:: or
a public address unless MORPHIT_MCP_ALLOW_PUBLIC_BIND=1; DNS-rebinding
Host/Origin allowlists enforced by both our middleware and the SDK
transport (browser Origins rejected by default; the default Host
allowlist auto-includes the bound address); a per-client token-bucket
rate limit; a hard request-body cap; a connection ceiling; slowloris
timeouts; and SSRF-guarded outbound fetches. The systemd unit adds a
@system-service seccomp allowlist, ProtectSystem=strict,
ReadOnlyPaths, an empty capability set, UMask=0077, and MemoryMax=256M,
and restarts forever with a start-limit circuit breaker.
Lifecycle. Fresh nodes: the Ansible role installs the (HTTP-mode) unit
and enables+starts it. Existing nodes: morphit-ops upgrade re-deploys the
MCP's isolated vendored tree and restarts it (gated on the unit being
installed), so the endpoint rolls forward automatically.
Source: apps/mcp-server/src/main.ts (transport selection +
startHttpTransport + in-file rate-limiter / Host-Origin / body-cap
middleware + bindAllowedByDefault), ops/systemd/morphit-mcp.service,
apps/ops-cli/src/commands/upgrade.ts (step 10b),
apps/mcp-server/scripts/mcp-http-transport-smoke.ts (12-scenario
behavioral test).