# ExMCP Architecture Guide
## Overview
ExMCP's architecture focuses on developer experience, type safety, and production readiness. This guide explains the architectural decisions and design patterns.
## Core Design Principles
### 1. Structured Responses
v2 introduces dedicated response types (`ExMCP.Response` and `ExMCP.Error`) to provide:
- **Type safety** - Clear response types instead of raw maps
- **Consistency** - All operations return the same response structure
- **Discoverability** - Helper functions for content extraction
```elixir
# v1: Raw maps with unclear structure
{:ok, %{"tools" => tools}} = Client.list_tools(client)
# v2: Structured responses with helpers
{:ok, response} = Client.list_tools(client)
tools = ExMCP.Response.tools(response)
```
### 2. Configuration Builder Pattern
The `ExMCP.ClientConfig` module provides a fluent interface for client configuration:
```elixir
config = ExMCP.ClientConfig.new()
|> ExMCP.ClientConfig.put_transport(:http)
|> ExMCP.ClientConfig.put_url("https://api.example.com")
|> ExMCP.ClientConfig.put_transport_options(timeout: 30_000)
```
Benefits:
- Compile-time validation of configuration
- Clear documentation of available options
- Immutable configuration objects
- Easy to extend with new options
### 3. Enhanced DSL
The v2 DSL modules provide:
- **Compile-time validation** of tool/resource/prompt definitions
- **Clear separation** between metadata and implementation
- **Consistent naming** aligned with MCP specification
```elixir
# Clear, declarative syntax
tool "search" do
description "Search the web"
input_schema %{...} # JSON Schema validation
handler fn args -> ... end
end
```
## Module Organization
### Core Modules
```
lib/ex_mcp_v2/
├── client.ex # Main client implementation
├── client_config.ex # Configuration builder
├── response.ex # Structured response type
├── error.ex # Structured error type
├── server_v2.ex # Enhanced server with transport support
├── http_plug.ex # HTTP/SSE transport integration
└── message_processor.ex # Core protocol handling (renamed from Plug)
```
### DSL Modules
```
lib/ex_mcp_v2/dsl/
├── tool.ex # Tool definition DSL
├── resource.ex # Resource definition DSL
├── prompt.ex # Prompt definition DSL
└── advanced.ex # Advanced DSL features
```
### Support Modules
```
lib/ex_mcp_v2/
├── transport_manager.ex # Transport lifecycle management
├── simple_client.ex # Simplified client for testing
├── convenience_client.ex # Top-level convenience functions
└── helpers.ex # Shared utilities
```
## Key Architectural Patterns
### 1. Transport Abstraction
The transport layer is completely abstracted from the protocol layer:
```elixir
# Transport manager handles connection lifecycle
defmodule TransportManager do
def connect(config) do
case config.transport do
:stdio -> StdioTransport.connect(config)
:http -> HttpTransport.connect(config)
end
end
end
```
### 2. Message Processing Pipeline
```
Request → Transport → MessageProcessor → Handler → Response → Transport
```
The `MessageProcessor` (formerly Plug) handles:
- JSON-RPC protocol encoding/decoding
- Method routing
- Error handling
- Response formatting
### 3. HTTP/SSE Integration
The `HttpPlug` module provides:
- Standard HTTP POST for requests
- SSE endpoint for server-initiated messages
- Backpressure control for slow clients
- Connection resumption via Last-Event-ID
```elixir
# Automatic SSE support
plug ExMCP.HttpPlug,
handler: MyHandler,
server_info: %{name: "server", version: "1.0.0"}
```
### 4. Error Handling Strategy
Errors are categorized into three types:
1. **Protocol Errors** - JSON-RPC level errors
2. **Application Errors** - Tool/resource execution errors
3. **Transport Errors** - Connection/network errors
```elixir
# Consistent error handling
case Client.call_tool(client, "tool", args) do
{:ok, response} when response.type == :error ->
# Application error (tool returned error)
handle_app_error(response.content)
{:ok, response} ->
# Success
process_response(response)
{:error, error} ->
# Protocol or transport error
handle_protocol_error(error)
end
```
## Production Features
### 1. SSE Backpressure Control
The SSE handler implements sophisticated flow control:
```elixir
defmodule SSEHandler do
@max_mailbox_size 1000
def handle_call(:request_send, from, state) do
{:message_queue_len, queue_len} = Process.info(self(), :message_queue_len)
if queue_len > @max_mailbox_size do
# Block producer until mailbox drains
{:noreply, %{state | producers: MapSet.put(state.producers, from)}}
else
{:reply, :ok, state}
end
end
end
```
### 2. Connection Resilience
- Automatic reconnection with exponential backoff
- Connection state tracking
- Graceful degradation on errors
### 3. Deprecation Management
v2 includes comprehensive deprecation warnings with source location:
```elixir
defmacro tool_description(desc) do
caller = __CALLER__
Logger.warning(
"tool_description/1 is deprecated. Use description/1 instead.",
file: Path.relative_to_cwd(caller.file),
line: caller.line
)
end
```
## Testing Architecture
### 1. Test Helpers
```elixir
defmodule ExMCP.TestHelpers do
def start_test_server(opts) do
# Creates an in-memory test server
end
def connect_test_client(server) do
# Connects directly without transport
end
end
```
### 2. Property-Based Testing
v2 includes property-based tests for:
- Protocol encoding/decoding
- Response type conversions
- Error categorization
### 3. Integration Testing
Comprehensive integration tests cover:
- Concurrent client connections
- SSE streaming behavior
- Error propagation
- Performance characteristics
## Performance Considerations
### 1. Zero-Copy Message Passing
When using stdio transport within the same BEAM:
- Messages are passed by reference
- No serialization overhead
- ~15μs latency for local calls
### 2. Connection Pooling
HTTP transport supports connection pooling:
```elixir
config |> ExMCP.ClientConfig.put_transport_options(
pool_size: 10,
pool_timeout: 5000
)
```
### 3. Streaming Support
SSE enables efficient streaming of:
- Progress updates
- Resource notifications
- Large responses
## Migration Path
### 1. Compatibility Layer
v2 maintains backwards compatibility through:
- Deprecated function warnings
- Automatic response conversion
- Legacy DSL support
### 2. Incremental Migration
Applications can migrate incrementally:
1. Update client code to use v2 API
2. Migrate DSL definitions
3. Update error handling
4. Remove deprecated calls
### 3. Feature Detection
```elixir
# Check for v2 features
if function_exported?(ExMCP, :client_config, 0) do
# Use v2 API
else
# Fall back to v1
end
```
## Future Extensibility
### 1. Custom Response Types
v2 response system is extensible:
```elixir
defmodule MyApp.CustomResponse do
def custom_type(data, source) do
%ExMCP.Response{
type: :custom,
content: data,
source: source
}
end
end
```
### 2. Transport Plugins
New transports can be added by implementing the behaviour:
```elixir
defmodule MyTransport do
@behaviour ExMCP.Transport
def connect(opts), do: ...
def send_message(msg, state), do: ...
def receive_message(state), do: ...
def close(state), do: ...
end
```
### 3. Middleware Support
Future versions will support middleware:
```elixir
config |> ExMCP.ClientConfig.put_middleware([
ExMCP.Middleware.Logger,
ExMCP.Middleware.Retry,
MyApp.CustomMiddleware
])
```
## Best Practices
1. **Always use structured responses** in handlers
2. **Configure timeouts** appropriately for your use case
3. **Monitor SSE connections** for backpressure
4. **Use the DSL** for cleaner, validated definitions
5. **Handle errors** at the appropriate level
6. **Test with property-based tests** for edge cases
7. **Profile performance** for production workloads
## Conclusion
ExMCP's architecture prioritizes:
- **Developer experience** through clear APIs and helpful errors
- **Type safety** with structured responses
- **Production readiness** with backpressure and monitoring
- **Extensibility** for future enhancements
The modular design allows teams to adopt v2 features incrementally while maintaining compatibility with existing code.