ADR-004-F: MCP at Every Domain Boundary¶
Status: Accepted Date: 2026-04-16 Decision Makers: Gautham Chellappa Depends on: ADR-002-F (Modular Monolith), ADR-003-F (Three-tier agents)
Context¶
Agents need a uniform way to interact with 19 domain OTP apps. Without a contract, each domain grows a bespoke interface that agents must special-case. With a thin contract (direct function calls), the contract becomes coupled to Elixir internals — extracting a domain to another language would require rewriting every agent caller.
Model Context Protocol (MCP) is emerging as the open standard for agent-to-tool contracts — typed tool schemas, categorised actions, framework-injected context. Anthropic sponsors MCP and Claude natively supports it. Adopting MCP at every domain boundary gives us three properties at once.
Decision¶
Every domain OTP app exposes one MCP server with typed tools. Agents interact with domains only via these tools. Inside a single BEAM node, MCP is an Elixir behaviour — tool calls are direct function calls for µs latency (no JSON-RPC overhead). The protocol shape (typed schemas, category metadata, context injection) is preserved; only the transport is elided.
apps/finnest_<domain>/lib/finnest_<domain>/mcp/
├── server.ex # MCP server registration
└── tools/
├── list_shifts.ex # one file per tool
├── propose_assignment.ex
└── ...
Tool shape (gold standard):
defmodule Finnest.Roster.MCP.Tools.ListShifts do
use Finnest.Agents.MCP.Tool,
name: "roster_list_shifts",
domain: :roster,
category: :read,
description: "..."
input :date_from, :date, required: true
input :date_to, :date, required: true
input :site_id, :uuid, required: false
output_schema %{ shifts: [...] }
def call(params, %{org_id: org_id} = _ctx) do
# org_id from context, NEVER from params
end
end
Four tool categories (AW-12): :read, :propose, :execute, :restricted.
org_id injected by framework from session.metadata — agents cannot provide it (AI-03).
Alternatives Considered¶
| Alternative | Rejected because |
|---|---|
| Direct Elixir function calls (agents import context modules) | Tight coupling to Elixir; extraction in Phase 3+ requires agent rewrite; no typed tool-schema metadata for LLM function-calling |
| gRPC between agents and domains | Not MCP-standard; adds protobuf toolchain; LLMs don't speak protobuf natively |
| REST HTTP calls (in-process loopback) | JSON parsing overhead for every call; no typed contract; invents a protocol worse than MCP |
| JSON-RPC over local socket even within one node | ~100µs per call vs ~1µs for direct; unnecessary at current scale. Part 6 decision: defer until domain extraction justifies the transport |
Consequences¶
Positive:
- Uniform agent-to-domain contract — agents don't need 19 bespoke interfaces
- Typed tool schemas enable Claude's function-calling to use tools without glue code
- Domain extraction (D12 phase 3) requires only a transport swap (behaviour → JSON-RPC adapter) — agent code unchanged
- Permission matrix and IRAP restrictions centralised at MCP framework level
- Agent audit trail naturally emerges from the framework (AW-14)
Negative:
- Developers must write an MCP tool entry for every capability agents need
- Tool schema drift risk if two tools in different domains diverge on naming conventions (mitigated by gold-standard template)
- Slight overhead (context injection, logging) per tool call vs raw function call — negligible
Tipping points for re-evaluation:
- MCP standard changes significantly in backward-incompatible ways — we'd likely pin a known-good version
- A domain is extracted to another language (D12 phase 3) — MCP transport swap is the planned path; this is a strengthening, not a rejection
Relationship to Guardrails¶
Enforces: AR-16 (MCP at every domain boundary), AI-03 (tenant injection at MCP layer), AI-04 (agent processes never reused across tenants).