# Architecture Document: MCP Elixir SDK

## Document Info
- **Project**: MCP Elixir SDK (Hex package `mcp_elixir_sdk`)
- **Version**: 1.0.2
- **Date**: 2026-02-09
- **Status**: Phase 7 Complete — 100% Conformance (Tier 1)
- **Protocol**: MCP 2025-11-25

---

## 1. Protocol Overview

MCP uses JSON-RPC 2.0 over stateful connections. The protocol has three phases:

```
1. Initialization    Client sends initialize request
                     Server responds with capabilities
                     Client sends initialized notification

2. Operation         Bidirectional JSON-RPC messages
                     Based on negotiated capabilities

3. Shutdown          Transport-level disconnection
```

### Roles

```
Host Application
  |
  +-- Client 1 ←→ Server A (tools: weather, stocks)
  +-- Client 2 ←→ Server B (resources: files, git)
  +-- Client 3 ←→ Server C (prompts: code review)
```

- **Host**: Application containing one or more clients
- **Client**: Maintains 1:1 session with a server. Provides sampling, roots, elicitation to server.
- **Server**: Provides tools, resources, prompts to client. May request sampling/elicitation from client.

---

## 2. Module Map

```
lib/mcp/
  # === Core Protocol (Phase 1 - COMPLETE) ===
  protocol.ex                        # JSON-RPC 2.0 encoding/decoding
  protocol/
    error.ex                         # MCP error codes + JSON-RPC errors
    methods.ex                       # Method name constants
    types/
      tool.ex                        # Tool struct
      tool_annotations.ex            # ToolAnnotations struct
      resource.ex                    # Resource struct
      resource_template.ex           # ResourceTemplate struct
      resource_contents.ex           # ResourceContents struct
      prompt.ex                      # Prompt struct
      prompt_argument.ex             # PromptArgument struct
      prompt_message.ex              # PromptMessage struct
      sampling_message.ex            # SamplingMessage struct
      model_preferences.ex           # ModelPreferences struct
      model_hint.ex                  # ModelHint struct
      root.ex                        # Root struct
      implementation.ex              # Implementation struct (client/server info)
      annotations.ex                 # Content Annotations struct
      icon.ex                        # Icon struct
      content.ex                     # Content type dispatcher
      content/
        text_content.ex              # TextContent struct
        image_content.ex             # ImageContent struct
        audio_content.ex             # AudioContent struct
        embedded_resource.ex         # EmbeddedResource struct
        resource_link.ex             # ResourceLink struct
    capabilities/
      server_capabilities.ex         # ServerCapabilities struct
      client_capabilities.ex         # ClientCapabilities struct
      tool_capabilities.ex           # ToolCapabilities struct
      resource_capabilities.ex       # ResourceCapabilities struct
      prompt_capabilities.ex         # PromptCapabilities struct
      logging_capabilities.ex        # LoggingCapabilities struct
      completion_capabilities.ex     # CompletionCapabilities struct
      sampling_capabilities.ex       # SamplingCapabilities struct
      root_capabilities.ex           # RootCapabilities struct
      elicitation_capabilities.ex    # ElicitationCapabilities struct
    messages/
      request.ex                     # JSON-RPC Request struct
      response.ex                    # JSON-RPC Response struct
      notification.ex                # JSON-RPC Notification struct
      initialize.ex                  # Initialize Params + Result
      ping.ex                        # Ping Params
      tools.ex                       # Tools ListParams/ListResult/CallParams/CallResult
      resources.ex                   # Resources List/Read/Subscribe/Templates types
      prompts.ex                     # Prompts List/Get types
      sampling.ex                    # Sampling CreateMessage Params/Result
      roots.ex                       # Roots List Params/Result
      elicitation.ex                 # Elicitation Params/Result
      logging.ex                     # Logging SetLevel/Message types
      completion.ex                  # Completion Params/Result
      notifications.ex               # Progress/Cancelled/ResourceUpdated params

  # === Transport Layer (Phase 2 + Phase 5 - COMPLETE) ===
  transport.ex                       # Transport behaviour (start_link, send_message, close)
  transport/
    stdio.ex                         # Port-based stdin/stdout transport (client + server modes)
    sse.ex                           # SSE encoding/decoding utilities
    streamable_http/
      client.ex                      # HTTP POST + SSE client transport (Req)
      server.ex                      # Server-side transport GenServer (bridges Plug ↔ MCP.Server)
      plug.ex                        # Plug endpoint handling POST/GET/DELETE HTTP methods
      pre_started.ex                 # Transport adapter for reusing existing transport pid

  # === Client (Phase 3 - COMPLETE) ===
  client.ex                          # High-level client API (GenServer)

  # === Server (Phase 4 + Phase 7 - COMPLETE) ===
  server.ex                          # High-level server API (GenServer, async tool support)
  server/
    handler.ex                       # Behaviour for tool/resource/prompt handlers
    tool_context.ex                  # Context for async tool handlers (Phase 7)
```

---

## 3. Transport Architecture

### Transport Behaviour

```elixir
@callback start_link(opts :: keyword()) :: GenServer.on_start()
@callback send_message(pid :: pid(), message :: map()) :: :ok | {:error, term()}
@callback close(pid :: pid()) :: :ok
```

Transports run as GenServer processes. The owner receives messages via:
- `{:mcp_message, decoded_map}` — incoming JSON-RPC message
- `{:mcp_transport_closed, reason}` — transport closed/disconnected

### Stdio Transport

```
MCP.Client (GenServer)
  |
  +-- Port (stdin/stdout to subprocess)
  |     Write: JSON + newline to stdin
  |     Read: Newline-delimited JSON from stdout
  |     Stderr: Logged (not protocol messages)
  |
  +-- MCP Server Process (child)
```

- Client launches server as subprocess via `Port.open/2`
- Messages are newline-delimited JSON-RPC (no embedded newlines)
- Server's stderr is captured/logged but not parsed as protocol

### Streamable HTTP Transport (Phase 5 - COMPLETE)

```
Client Side:                          Server Side:
MCP.Client (GenServer)               StreamableHTTP.Plug (Plug endpoint)
  |                                     |
  +-- StreamableHTTP.Client             +-- POST → route_post → deliver_message
  |   (GenServer, Transport)            |     → StreamableHTTP.Server (GenServer)
  |   Sends HTTP POST (Req)             |     → MCP.Server (handler callbacks)
  |   Parses JSON or SSE response       |     → response routed back to caller
  |                                     |
  |   Headers:                          +-- GET → SSE stream (server-initiated)
  |   Content-Type: application/json    |
  |   Accept: application/json,         +-- DELETE → terminate session
  |           text/event-stream         |
  |   MCP-Session-Id: <sid>             +-- ETS session registry
  |   MCP-Protocol-Version: 2025-11-25 |     {session_id → transport_pid}
  |                                     |
  +-- On close: HTTP DELETE             +-- PreStarted adapter
                                              (reuses transport pid for MCP.Server)
```

Architecture overview:
- **Client**: `StreamableHTTP.Client` GenServer implements Transport behaviour.
  Sends JSON-RPC via `Req.post/2`, parses `application/json` or SSE responses.
  Extracts `MCP-Session-Id` from initialize response, includes in all subsequent requests.
  On close, sends HTTP DELETE to terminate session.

- **Server**: Three-module design:
  - `StreamableHTTP.Plug` — Plug handling POST/GET/DELETE HTTP methods. Creates sessions
    (transport + MCP.Server pairs) on initialize. Routes requests via ETS session registry.
  - `StreamableHTTP.Server` — Transport GenServer bridging Plug ↔ MCP.Server. Stores
    pending response callers so responses can be routed back to the correct HTTP connection.
  - `StreamableHTTP.PreStarted` — Adapter that lets MCP.Server reuse an already-started
    transport process (since the Plug starts the transport before the MCP.Server).
  - **`handler_opts` seam (1.1.0):** the Plug threads request-scoped identity from the
    authenticated Plug pipeline into `Handler.init/1` — a static keyword list or a per-session
    `(Plug.Conn.t() -> keyword())` factory evaluated once at `initialize`. Backward-compatible
    (absent `handler_opts` = prior behaviour). See
    [`handler-opts-identity-seam-spec.md`](https://github.com/JohnSmall/mcp-elixir-sdk/blob/main/docs/handler-opts-identity-seam-spec.md).

- **SSE**: `MCP.Transport.SSE` provides encoding/decoding utilities:
  - `encode_event/1`, `encode_message/2` for SSE event creation
  - `decode_event/1` for parsing SSE event text
  - `new_parser/0`, `feed/2` for incremental/chunked SSE stream parsing

- **Session management**: Server generates UUID session IDs, stores in ETS. Client extracts
  from `MCP-Session-Id` response header. Protocol version validated via `MCP-Protocol-Version`.

- **Handshake ordering**: a session's `MCP.Server` stays `:waiting` until it receives
  `notifications/initialized`; clients MUST drive `initialize → notifications/initialized →
  tools/call` (requests before `:ready` are rejected with "Server not initialized"). This is the
  most common consumer stumble. `MCP.Client.connect/1` does this automatically; raw HTTP clients
  must send `notifications/initialized` themselves.

- **Dependencies**: req ~> 0.5 (HTTP client), plug ~> 1.16 (HTTP framework),
  bandit ~> 1.5 (HTTP server) — all optional, only needed for Streamable HTTP

---

## 4. Client Architecture

```
MCP.Client (GenServer)
  |
  +-- state:
  |     transport_module / transport_pid — the transport process
  |     server_capabilities: ServerCapabilities.t()
  |     server_info: Implementation.t()
  |     client_info / client_capabilities — sent during initialization
  |     pending_requests: %{id => {from, timeout_ref}}
  |     next_id: integer (incrementing)
  |     status: :disconnected | :initializing | :ready | :closed
  |     notification_handler: pid | (method, params -> any)
  |     request_handlers: %{method => callback_fn}
  |
  +-- Public API:
  |     start_link/1         → create GenServer + start transport
  |     connect/1-2          → initialize handshake
  |     list_tools/2         → tools/list
  |     call_tool/3-4        → tools/call
  |     list_resources/2     → resources/list
  |     read_resource/2-3    → resources/read
  |     list_resource_templates/2 → resources/templates/list
  |     subscribe_resource/2-3    → resources/subscribe
  |     unsubscribe_resource/2-3  → resources/unsubscribe
  |     list_prompts/2       → prompts/list
  |     get_prompt/3-4       → prompts/get
  |     ping/1-2             → ping (works pre-init)
  |     close/1              → shutdown
  |     list_all_tools/2     → paginated tools/list
  |     list_all_resources/2 → paginated resources/list
  |     list_all_prompts/2   → paginated prompts/list
  |
  +-- Incoming (from server):
        notifications → dispatch to notification_handler (pid or function)
        requests (sampling, elicitation) → dispatch to request_handlers map
```

### Request/Response Matching

Client assigns incrementing integer IDs to outgoing requests. Each pending request stores `{from, timeout_ref}` in a map. When a response arrives via `{:mcp_message, decoded}`, `Protocol.decode_message/1` classifies it and the matching ID resolves the pending `GenServer.call/3` via `GenServer.reply/2`. Timeouts use `Process.send_after/3`.

### Server-Initiated Requests

MCP servers can send requests to clients (sampling, roots, elicitation). The client dispatches these to callback functions provided at start:

```elixir
{:ok, client} = MCP.Client.start_link(
  transport: {MCP.Transport.Stdio, command: "server", args: []},
  client_info: %{name: "my_app", version: "1.0.0"},
  request_handlers: %{
    "sampling/createMessage" => fn _method, params -> {:ok, result} end,
    "roots/list" => fn _method, _params -> {:ok, %{"roots" => []}} end
  },
  notification_handler: self()  # or fn method, params -> ... end
)
```

---

## 5. Server Architecture

```
MCP.Server (GenServer)
  |
  +-- state:
  |     handler_module / handler_state — user's Handler behaviour implementation
  |     transport_module / transport_pid — the transport process
  |     client_capabilities: ClientCapabilities.t()
  |     client_info: Implementation.t()
  |     server_info / capabilities / instructions — declared at startup
  |     status: :waiting | :ready | :closed
  |     pending_requests: %{id => {from, timeout_ref}}
  |     next_id: integer (incrementing, for server-initiated requests)
  |     log_level: current log level set by client
  |
  +-- Public API:
  |     start_link/1          → create GenServer + start transport + init handler
  |     close/1               → shutdown
  |     transport/1, status/1 → accessors
  |     client_capabilities/1, client_info/1 → from initialization
  |
  +-- Notifications (server → client):
  |     notify_tools_changed/1     → notifications/tools/list_changed
  |     notify_resources_changed/1 → notifications/resources/list_changed
  |     notify_resource_updated/2  → notifications/resources/updated (with uri)
  |     notify_prompts_changed/1   → notifications/prompts/list_changed
  |     log/3-4                    → notifications/message (respects log level)
  |     send_progress/3-4          → notifications/progress
  |
  +-- Server-initiated requests (server → client):
  |     request_sampling/2-3       → sampling/createMessage
  |     request_roots/1-2          → roots/list
  |     request_elicitation/2-3    → elicitation/create
  |
  +-- Incoming (from client):
  |     initialize → respond with capabilities, store client info
  |     notifications/initialized → transition to :ready
  |     ping → empty response (works pre-init)
  |     tools/list, tools/call → dispatch to handler
  |     resources/list, resources/read → dispatch to handler
  |     resources/subscribe, resources/unsubscribe → dispatch to handler
  |     resources/templates/list → dispatch to handler
  |     prompts/list, prompts/get → dispatch to handler
  |     completion/complete → dispatch to handler
  |     logging/setLevel → dispatch to handler + update log_level
  |     unknown method → -32601 error
```

### Handler Behaviour

`MCP.Server.Handler` defines optional callbacks for all server features.
The server auto-detects capabilities by inspecting which callbacks the
handler module exports via `__info__(:functions)`.

```elixir
@callback init(opts) :: {:ok, state}
@callback handle_list_tools(cursor, state) :: {:ok, tools, next_cursor, state}
@callback handle_call_tool(name, arguments, state) :: {:ok, content, state} | {:error, code, msg, state}
@callback handle_call_tool(name, arguments, context, state) :: {:ok, content, state} | {:error, code, msg, state}  # async (Phase 7)
@callback handle_list_resources(cursor, state) :: {:ok, resources, next_cursor, state}
@callback handle_read_resource(uri, state) :: {:ok, contents, state} | {:error, code, msg, state}
@callback handle_subscribe(uri, state) :: {:ok, state} | {:error, code, msg, state}
@callback handle_unsubscribe(uri, state) :: {:ok, state} | {:error, code, msg, state}
@callback handle_list_resource_templates(cursor, state) :: {:ok, templates, next_cursor, state}
@callback handle_list_prompts(cursor, state) :: {:ok, prompts, next_cursor, state}
@callback handle_get_prompt(name, arguments, state) :: {:ok, result, state} | {:error, code, msg, state}
@callback handle_complete(ref, argument, state) :: {:ok, completion, state}
@callback handle_set_log_level(level, state) :: {:ok, state}
```

### Request Routing

Routing is inline in the Server GenServer via pattern-matched function clauses
on `%Request{method: "tools/list"}` etc. No separate Router module needed —
Elixir's pattern matching makes this clean and Credo-friendly.

### Async Tool Execution (Phase 7)

Tools that implement `handle_call_tool/4` (with `ToolContext`) execute asynchronously
in a spawned `Task`. This allows tools to send intermediate messages during execution:

```
Client                      Plug                    Server                  Handler Task
  |                           |                       |                       |
  +-- POST tools/call ------->|                       |                       |
  |                           +-- register_stream --->|                       |
  |                           +-- deliver_async ----->|                       |
  |                           |                       +-- Task.async -------->|
  |                           |                       |                       |
  |                           |                       |<-- context_notify ----|  (log)
  |                     {sse_event} <-- send_message -|                       |
  |<-- SSE: log notification -|                       |                       |
  |                           |                       |<-- context_request ---|  (sampling)
  |                     {sse_event} <-- send_message -|                       |
  |<-- SSE: sampling request -|                       |                       |
  |                           |                       |                       |
  +-- POST sampling response->|                       |                       |
  |                           +-- deliver_message --->|                       |
  |                           |                       +-- reply to request -->|
  |                           |                       |                       |
  |                           |                       |<-- Task completes ----|
  |                     {sse_done} <-- send_message --|                       |
  |<-- SSE: tool result ------| (stream closed)       |                       |
```

Key components:
- `MCP.Server.ToolContext` — context struct with `server_pid`, `request_id`, `meta`
- `handle_call_tool/4` — async callback detected via `__info__(:functions)`
- HTTPTransport `send_message/3` — opts `[related_request_id: id]` routes to correct SSE stream
- Plug `stream_loop/1` — chunked SSE receive loop for `{:sse_event, data}` and `{:sse_done, data}`

### Capability Auto-Detection

The server inspects `handler_module.__info__(:functions)` to detect which
callbacks are implemented, then builds `%ServerCapabilities{}` accordingly:
- `handle_list_tools/2` → tools capability (with listChanged)
- `handle_list_resources/2` → resources capability (with listChanged)
- `handle_subscribe/2` → resources.subscribe capability
- `handle_list_prompts/2` → prompts capability (with listChanged)
- `handle_set_log_level/2` → logging capability
- `handle_complete/3` → completions capability

---

## 6. JSON-RPC 2.0 Message Types

### Request
```json
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
```

### Response (success)
```json
{"jsonrpc": "2.0", "id": 1, "result": {"tools": [...]}}
```

### Response (error)
```json
{"jsonrpc": "2.0", "id": 1, "error": {"code": -32602, "message": "Unknown tool"}}
```

### Notification (no response expected)
```json
{"jsonrpc": "2.0", "method": "notifications/tools/list_changed"}
```

### Standard Error Codes
| Code | Meaning |
|------|---------|
| -32700 | Parse error |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| -32002 | Resource not found |
| -32042 | URL elicitation required |
| -1 | User rejected sampling |

---

## 7. Capability Negotiation

During initialization, both sides declare what they support:

### Server Capabilities
```elixir
%ServerCapabilities{
  tools: %{listChanged: true},
  resources: %{subscribe: true, listChanged: true},
  prompts: %{listChanged: true},
  logging: %{},
  completions: %{}
}
```

### Client Capabilities
```elixir
%ClientCapabilities{
  roots: %{listChanged: true},
  sampling: %{tools: %{}},
  elicitation: %{form: %{}, url: %{}}
}
```

Both sides MUST respect declared capabilities throughout the session.

---

## 8. Content Types

Tool results, prompts, and resources can contain multiple content types:

| Type | Fields | Usage |
|------|--------|-------|
| `TextContent` | type: "text", text | Most common |
| `ImageContent` | type: "image", data (base64), mimeType | Visual content |
| `AudioContent` | type: "audio", data (base64), mimeType | Audio content |
| `ResourceContent` | type: "resource", resource (uri, text/blob) | Embedded resources |
| `ResourceLink` | type: "resource_link", uri, name, mimeType | Links to resources |

---

## 9. Elixir/OTP Design Patterns

| MCP Concept | Elixir Implementation |
|-------------|----------------------|
| Client session | GenServer per connection |
| Server instance | GenServer per connection |
| Stdio transport | Port (Erlang port for subprocess) |
| SSE stream | `Req` + stream processing / `Plug.Conn` chunked |
| Request/response matching | Map of `%{id => from}` in GenServer state |
| Notifications | `send/2` to registered handler processes |
| Tool registration | Map in GenServer state |
| JSON-RPC framing | `Jason.encode!/1` + `Jason.decode!/1` |
| Session lifecycle | GenServer init/handle_call/terminate |
| Concurrent clients | Supervisor with dynamic children |
| Pagination | Cursor-based, lazy with Stream |

---

## 10. Testing Strategy

### Unit Tests
- Protocol encoding/decoding (JSON-RPC messages)
- Type serialization/deserialization
- Capability negotiation logic
- Transport message framing (stdio, HTTP)
- Client API (with mock transport)
- Server API (with mock transport)

### Integration Tests
- Client ↔ Server over stdio (in-process)
- Client ↔ Server over HTTP (localhost)
- Full lifecycle: init → operations → shutdown

### Conformance Tests
- Official MCP conformance suite via `npx @modelcontextprotocol/conformance`
- Server mode: conformance framework connects to our server
- Client mode: conformance framework tests our client
- Expected failures baseline file for incremental compliance
- GitHub Actions integration for CI

---

## 11. Dependencies

### Required
| Dep | Purpose |
|-----|---------|
| `jason` | JSON encoding/decoding |
| `elixir_uuid` | ID generation |

### Optional
| Dep | Purpose | When Needed |
|-----|---------|-------------|
| `req` | HTTP client | Streamable HTTP client transport |
| `plug` | HTTP server framework | Streamable HTTP server transport |
| `bandit` | HTTP server | Streamable HTTP server transport |
| `castore` | TLS certificates | HTTPS connections |

### Dev/Test
| Dep | Purpose |
|-----|---------|
| `dialyxir` | Type checking |
| `credo` | Static analysis |
| `ex_doc` | Documentation |
