Packages
ex_mcp
1.5.0
1.7.2
1.7.0
retired
1.6.2
retired
1.6.1
retired
1.6.0
retired
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.0
retired
1.1.1
retired
1.1.0
retired
1.0.0
retired
1.0.0-rc.8
retired
1.0.0-rc.7
retired
1.0.0-rc.6
retired
1.0.0-rc.5
retired
1.0.0-rc.4
retired
1.0.0-rc.2
retired
1.0.0-rc.1
retired
1.0.0-rc.0
retired
0.12.0
retired
0.11.0
retired
0.10.0
retired
0.9.2
retired
0.9.1
retired
0.9.0
retired
0.8.4
retired
0.8.3
retired
0.8.2
retired
0.8.1
retired
0.8.0
retired
0.7.4
retired
0.7.3
retired
0.7.1
retired
0.7.0
retired
0.1.0
retired
Elixir implementation of MCP and ACP. Build MCP clients/servers with tools, resources, prompts over stdio, HTTP/SSE, and BEAM. Control coding agents via ACP with adapters for Claude Code, Codex, and more.
Retired package: Security issue - Request cancellation not session-scoped (GHSA-22m7-hr37-pxg3). Upgrade to ex_mcp 1.6.2.
Current section
Files
Jump to
Current section
Files
docs/ARCHITECTURE.md
# ExMCP Architecture Guide
ExMCP is organized around protocol boundaries: clients, servers, transports,
HTTP Plug integration, ACP, authorization, and internal protocol helpers. Public
APIs stay small; cross-cutting work is kept at transport or Plug boundaries.
This guide describes the 1.x architecture. The accepted direction for
per-server runtime ownership, bounded handler scheduling, and replaceable
state stores is tracked separately in the
[ExMCP 2.0 roadmap](https://github.com/azmaveth/ex_mcp/blob/master/docs/V2_ROADMAP.md).
## Public Layers
### MCP Client
`ExMCP.Client` owns the client process, protocol-era establishment, request
IDs, server capability state, retries, and request/response formatting.
Client operation modules under `lib/ex_mcp/client/operations/` keep tool,
resource, and prompt calls focused while `ExMCP.Client.ConnectionManager`
normalizes transport startup.
### MCP Server
Server implementations use `ExMCP.Server.Handler`. Most applications should add
`ExMCP.Server.DSL` for declarative tools, resources, and prompts:
```elixir
defmodule MyServer do
use ExMCP.Server.Handler
use ExMCP.Server.DSL
tool "echo", "Echo input" do
param :message, :string, required: true
run fn %{message: message}, state ->
{:ok, %{content: [%{type: "text", text: message}]}, state}
end
end
end
```
`ExMCP.Server.HandlerServer` is the transport-aware process for in-memory and
BEAM-local handler execution. HTTP and stdio servers are started through
`ExMCP.Server.Transport` or the DSL-generated `start_link/1`.
### Transports
Transport modules implement `ExMCP.Transport`:
- `ExMCP.Transport.Stdio` for newline-delimited JSON-RPC over subprocess stdio.
- `ExMCP.Transport.HTTP` for legacy and modern Streamable HTTP. Legacy
revisions may use a standalone GET SSE stream; modern SSE belongs to its
originating POST.
- `ExMCP.Transport.Local` for BEAM-local MCP maps/lists passed as Elixir terms.
- `ExMCP.Transport.Test` for in-memory tests.
BEAM-local MCP is selected with `transport: :beam` and requires a server PID:
```elixir
{:ok, server} = MyServer.start_link(transport: :beam) # or HandlerServer.start_link(handler: MyHandler, ...)
{:ok, client} = ExMCP.Client.start_link(transport: :beam, server: server)
```
The removed `:native` alias and direct dispatcher API are not part of the 1.0
public architecture.
### HTTP Plug
`ExMCP.HttpPlug` is the HTTP server boundary. Request parsing, session
resolution, CORS/origin handling, response shaping, and SSE handling are split
under `lib/ex_mcp/http_plug/`.
Use normal Phoenix/Plug composition for HTTP edge concerns:
```elixir
pipeline :mcp do
plug ExMCP.Plugs.DnsRebinding
plug MyApp.AuthenticateMCP
end
scope "/mcp" do
pipe_through :mcp
forward "/", ExMCP.HttpPlug,
handler: MyApp.MCPServer,
server_info: %{name: "my-app", version: "1.0.0"}
end
```
### ACP
ACP modules live under `lib/ex_mcp/acp/`. `ExMCP.ACP.Client` controls ACP
agents, `ExMCP.ACP.Agent` exposes native Elixir ACP agents, and adapter modules
bridge external CLIs such as Claude Code, Codex, and Pi.
ACP pooling is intentionally left to consumers. ExMCP provides the protocol,
transport, adapter, and native-agent building blocks.
## Protocol Era Model
ExMCP treats MCP 2025-11-25 and earlier as the **legacy era** and MCP
2026-07-28 as the **modern era**. This is an architectural boundary, not just a
version comparison: handshake, metadata, result envelopes, notifications, and
HTTP state all change together. A connection is established in one era and is
never allowed to mix wire shapes afterward.
```mermaid
flowchart TD
A["Client starts with protocol_mode"] --> B{"Opening policy"}
B -->|"legacy_only"| C["initialize"]
B -->|"prefer_legacy"| C
B -->|"prefer_modern"| D["server/discover probe"]
B -->|"modern_only"| D
C -->|"success"| E["Pin legacy era"]
C -->|"eligible protocol failure + live transport"| D
D -->|"valid discovery"| F["Pin modern era"]
D -->|"evidence of legacy + fallback allowed"| C
D -->|"ambiguous, transport, or strict-mode failure"| G["Fail connection"]
E --> H["Legacy request/result rules"]
F --> I["Modern metadata/result rules"]
```
The four modes are intentionally policies rather than protocol versions:
| Mode | Enabled eras | Client opens with | Automatic fallback |
|---|---|---|---|
| `:legacy_only` | Legacy | `initialize` | Never |
| `:prefer_legacy` | Both | `initialize` | To modern only after an eligible protocol failure on a live transport |
| `:prefer_modern` | Both | `server/discover` | To legacy only with positive compatibility evidence on a live transport |
| `:modern_only` | Modern | `server/discover` | Never |
On the server, both `:prefer_legacy` and `:prefer_modern` accept either era;
the preference controls advertised version ordering. HandlerServer and stdio
connections pin on a valid modern request or legacy `initialize`. HTTP remains
stateless in the modern era, so `ExMCP.Server.RequestContext` derives and
validates the era on each request instead.
### Era responsibilities
- ExMCP.Internal.VersionRegistry is the source of truth for known revisions,
their era, enabled versions, and preference order. The zero-arity legacy
helpers deliberately retain rc.5 behavior during the RC soak.
- `ExMCP.Client.ConnectionManager` applies the selected policy.
`ExMCP.Client.EraProbe` owns the bounded, side-effect-free
`server/discover` probe.
- `ExMCP.Client.EraCache` keys observations by transport identity. Modern
observations do not expire and cannot be replaced by automatic downgrade;
legacy observations expire so an upgraded peer is eventually probed again.
- `ExMCP.Server.RequestContext` separates modern `_meta` from application
parameters, validates the configured mode and method availability, and
exposes a single context to dispatch.
- `ExMCP.Protocol.ResultEnvelope` enforces modern `resultType` while preserving
the legacy result shape. MRTR continuation state, cache hints, subscriptions,
and modern Tasks remain protocol-layer concerns rather than transport state.
- Transports implement era-specific framing only. When HTTP settles modern it
discards legacy session state and disables the standalone GET stream.
This separation prevents a failed modern probe from becoming an unsafe silent
downgrade. Fallback requires both a recognized compatibility signal and a
still-usable transport. A cached modern peer failing its next probe is surfaced
as an error until the operator clears that observation or changes policy.
## Internal Functional Cores
Pure transformation and validation logic is kept separate from process and I/O
boundaries:
- Internal protocol and version modules handle message construction, parsing,
and version rules.
- The message processor modules provide a Plug-like processing pipeline for
server request dispatch.
- `ExMCP.Content.*` modules normalize content, sanitize inputs, and validate
schema-related data.
- ACP mapper/protocol/session modules keep adapter-specific decoding separate
from subprocess ports.
This structure keeps side effects at the edges: GenServers, Ports, HTTP
requests, Plug connections, filesystem-backed session stores, and telemetry.
## Resilience And Pipelines
ExMCP currently has three pipeline-style boundaries:
- HTTP server requests: normal Plug/Phoenix pipelines around `ExMCP.HttpPlug`.
- Server message processing: `ExMCP.MessageProcessor.run/2` for internal
Plug-like request processing.
- Transport reliability: `ExMCP.Transport.ReliabilityWrapper`, client
`retry_policy`, and `ExMCP.Reliability.*` components.
HTTP client connection handling is transport-owned today. If ExMCP later adds a
public client middleware API, it should wrap request construction and transport
send/receive at the `ExMCP.Client` boundary rather than inside HTTP-specific
code, so stdio, HTTP, and BEAM-local can share the same cross-cutting behavior.
## Module Map
```text
lib/ex_mcp/
acp/ ACP protocol, client, native agent, adapters
authorization/ OAuth 2.1 and auth provider flows
client/ Client operations, handlers, state, connection setup
content/ Content builders, validation, sanitization
http_plug/ HTTP Plug functional core and SSE handling
internal/ Private protocol, era/version, map, and security helpers
message_processor/ Plug-like MCP request processing
plugs/ Reusable Plug security/auth components
protocol/ Public protocol utility modules
reliability/ Retry, circuit breaker, health check supervisor
server/ Handler behavior, DSL, transport startup
transport/ Stdio, HTTP, BEAM-local, test transports
```
## Testing Architecture
The test suite covers unit, integration, interop, conformance, security, and
transport behavior. `ExMCP.Transport.Test` and `transport: :beam` keep local
server/client tests fast without starting subprocesses or network listeners.
External conformance scripts live in `scripts/` and should be run for each
supported MCP spec version before release:
```bash
./scripts/conformance.sh # published legacy/core harness
./scripts/conformance.sh all-versions # all negotiated legacy MCP versions
./scripts/conformance.sh modern # gating 2026-07-28 complete suites
./scripts/conformance.sh draft-alpha # non-gating future-draft exploration
mix mcp.sync_spec --version 2026-07-28 --force # refresh local docs/mcp-specs
```
### Protocol version alignment
| Protocol era | Revisions | ExMCP 1.0 RC support |
|---|---|---|
| MCP legacy | `2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25` | Enabled by `:legacy_only` and both preference modes |
| MCP modern | `2026-07-28` (latest stable) | Implemented; enabled by `:modern_only` and both preference modes |
| ACP | major `1` | `protocolVersion: 1` |
## Design Rules
- Prefer `ExMCP.Server.Handler` plus `ExMCP.Server.DSL` for servers.
- Select an explicit protocol mode in deployments and tests; never infer an
era solely from a method name after a connection has pinned.
- Keep compatibility fallback in `ExMCP.Client.ConnectionManager` and
`ExMCP.Client.EraProbe`; application operations must not implement their own
modern-to-legacy retry.
- Use `transport: :beam` for local BEAM MCP, not a separate service dispatcher.
- Put HTTP authorization, origin, and request-signing checks in Plug pipelines.
- Put transport failure handling in client retry/reliability options.
- Keep pure protocol transformations in functional modules and side effects in
GenServer, Port, Plug, or filesystem boundaries.
- Treat the 2.0 target architecture as future work; do not describe planned
runtime ownership or scheduler behavior here until it is implemented.