ExMCP Architecture Guide
View SourceOverview
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
# 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:
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
# Clear, declarative syntax
tool "search" do
description "Search the web"
input_schema %{...} # JSON Schema validation
handler fn args -> ... end
endModule 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 featuresSupport 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 utilitiesKey Architectural Patterns
1. Transport Abstraction
The transport layer is completely abstracted from the protocol layer:
# 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
end2. Message Processing Pipeline
Request → Transport → MessageProcessor → Handler → Response → TransportThe 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
# 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:
- Protocol Errors - JSON-RPC level errors
- Application Errors - Tool/resource execution errors
- Transport Errors - Connection/network errors
# 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)
endProduction Features
1. SSE Backpressure Control
The SSE handler implements sophisticated flow control:
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
end2. 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:
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
)
endTesting Architecture
1. Test Helpers
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
end2. 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:
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:
- Update client code to use v2 API
- Migrate DSL definitions
- Update error handling
- Remove deprecated calls
3. Feature Detection
# Check for v2 features
if function_exported?(ExMCP, :client_config, 0) do
# Use v2 API
else
# Fall back to v1
endFuture Extensibility
1. Custom Response Types
v2 response system is extensible:
defmodule MyApp.CustomResponse do
def custom_type(data, source) do
%ExMCP.Response{
type: :custom,
content: data,
source: source
}
end
end2. Transport Plugins
New transports can be added by implementing the behaviour:
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: ...
end3. Middleware Support
Future versions will support middleware:
config |> ExMCP.ClientConfig.put_middleware([
ExMCP.Middleware.Logger,
ExMCP.Middleware.Retry,
MyApp.CustomMiddleware
])Best Practices
- Always use structured responses in handlers
- Configure timeouts appropriately for your use case
- Monitor SSE connections for backpressure
- Use the DSL for cleaner, validated definitions
- Handle errors at the appropriate level
- Test with property-based tests for edge cases
- 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.