# MCP - Model Context Protocol Server
An Elixir implementation of the [Model Context Protocol (MCP)](https://spec.modelcontextprotocol.io/) server that communicates via Server-Sent Events (SSE) over HTTP.
## Overview
This library provides a complete MCP server implementation built on OTP principles using GenServers, supervision trees, and fault tolerance. It enables you to create custom tools that AI models can discover and execute through a standardized protocol.
## Features
- **Standards Compliant**: Implements MCP specification 2024-11-05
- **SSE Transport**: Real-time communication via Server-Sent Events over HTTP
- **JSON-RPC 2.0**: Full support for request/response/notification patterns
- **Fault Tolerant**: Built on OTP with proper supervision and error handling
- **Session Management**: UUID-based session tracking with automatic cleanup
- **Async Tool Execution**: Non-blocking tool execution with proper timeout handling
- **Flexible Tool Definition**: Support for both atom and string keys in tool specifications
## Protocol Implementation Status
This section tracks implementation of the MCP 2024-11-05 specification for tools:
### ✅ Core Protocol
- [x] Server-Sent Events (SSE) transport layer
- [x] JSON-RPC 2.0 message protocol
- [x] Session management with unique session IDs
- [x] Connection lifecycle management
- [x] Protocol version negotiation (2024-11-05)
### ✅ Initialization
- [x] `initialize` method with protocol version validation
- [x] Server capabilities declaration
- [x] Client capabilities handling
- [x] Server info response (name, version)
- [x] `notifications/initialized` completion
### ✅ Tools Protocol
- [x] Tools capability declaration in server capabilities
- [x] Tool definition with name, description, input schema
- [x] Tool validation during initialization
- [x] `tools/list` method for tool discovery
- [x] `tools/call` method for tool execution
- [x] Pagination support with cursor parameter
- [x] Tool callback execution with proper response format
- [x] Error handling for unknown tools
- [x] Tool execution error reporting with `isError` flag
### ✅ Content Types
- [x] Text content support (`type: "text"`)
- [x] Content array responses
- [x] Error content with `isError` flag
### ✅ Error Handling
- [x] JSON-RPC 2.0 error responses
- [x] Protocol-level error codes (-32600, -32601, -32602)
- [x] Tool execution error handling
- [x] Invalid tool specification validation
- [x] Missing required parameter handling
### ✅ Security & Validation
- [x] Input validation for tool schemas
- [x] Tool specification validation (required fields)
- [x] JSON Schema compliance for input schemas
- [x] Argument validation during tool calls
### ❌ Not Implemented
- [ ] `listChanged` notification capability
- [ ] Image content support (`type: "image"`)
- [ ] Resource content support (`type: "resource"`)
- [ ] Advanced pagination features
- [ ] Rate limiting for tool invocations
- [ ] Access control mechanisms
- [ ] Tool execution timeouts
### 🔄 Partial Implementation
- [ ] **Tool input validation**: Basic validation implemented, but full JSON Schema validation against tool input schemas not enforced during `tools/call`
## Installation
Add `mcp` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:mcp, "~> 0.1.0"}
]
end
```
## Quick Start
### 1. Define Your Tools
Create an initialization callback that defines the tools available to MCP clients:
```elixir
defmodule MyApp.MCPTools do
def init_callback(_session_id, _init_params) do
tools = [
%{
name: "echo",
description: "Echoes back the input text",
input_schema: %{
"type" => "object",
"properties" => %{
"text" => %{
"type" => "string",
"description" => "Text to echo back"
}
},
"required" => ["text"]
},
callback: fn %{"text" => text} ->
{:ok, %{
content: [
%{type: "text", text: "Echo: #{text}"}
]
}}
end
},
%{
name: "get_time",
description: "Returns the current time",
input_schema: %{"type" => "object", "properties" => %{}},
callback: fn _args ->
time = DateTime.utc_now() |> DateTime.to_string()
{:ok, %{
content: [
%{type: "text", text: "Current time: #{time}"}
]
}}
end
}
]
{:ok, %{
server_info: %{
name: "My MCP Server",
version: "1.0.0"
},
tools: tools
}}
end
end
```
### 2. Set Up the Router
Configure the MCP router in your Phoenix endpoint or Plug pipeline:
```elixir
# In your Phoenix endpoint
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# Add the MCP router
plug MCP.Router, init_callback: &MyApp.MCPTools.init_callback/2
# Your other plugs...
end
```
Or use it standalone with Bandit:
```elixir
# In your application.ex
def start(_type, _args) do
children = [
# Start your app supervision tree
MCP.Application,
# Start the HTTP server
{Bandit,
plug: {MCP.Router, init_callback: &MyApp.MCPTools.init_callback/2},
scheme: :http,
port: 4000}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
### 3. Start Your Server
```bash
iex -S mix
```
Your MCP server will be available at `http://localhost:4000`.
## Usage
### Client Connection
Clients connect to your server by:
1. **Establishing SSE Connection**: `GET /` to start receiving server events
2. **Sending Messages**: `POST /message?sessionId=<session_id>` to send JSON-RPC messages
### Tool Specification Format
Tools must include these fields:
```elixir
%{
name: "tool_name", # Unique identifier
description: "What the tool does", # Human-readable description
input_schema: %{ # JSON Schema for parameters
"type" => "object",
"properties" => %{
"param1" => %{"type" => "string"}
},
"required" => ["param1"]
},
callback: fn args -> # Function to execute
# Tool logic here
{:ok, result} | {:error, reason}
end
}
```
### Tool Callback Return Format
Tool callbacks should return results in MCP format:
```elixir
# Success response
{:ok, %{
content: [
%{type: "text", text: "Result text"},
%{type: "image", data: "base64_image", mimeType: "image/png"}
]
}}
# Error response
{:error, "Error description"}
```
## Architecture
### Core Components
- **`MCP.Application`**: OTP application entry point and supervision
- **`MCP.Router`**: HTTP routing with Plug, handles SSE and JSON-RPC endpoints
- **`MCP.Connection`**: GenServer managing individual client connections and protocol state
- **`MCP.SSE`**: Server-Sent Events transport layer implementation
### Message Flow
1. Client establishes SSE connection via `GET /`
2. Server responds with session endpoint URL
3. Client sends `initialize` request with protocol version
4. Server validates and responds with capabilities and tools
5. Client sends `notifications/initialized` to complete handshake
6. Client can now call tools via `tools/call` requests
### Session Management
- Each connection gets a unique session ID
- Sessions are tracked in an OTP Registry
- Automatic cleanup on disconnection or timeout
- 30-second initialization timeout
- 30-minute inactivity timeout
## Configuration
### Environment Variables
Set these in your application configuration:
```elixir
# config/config.exs
config :mcp,
port: 4000,
host: "localhost"
```
### Development Commands
```bash
# Install dependencies
mix deps.get
# Compile project
mix compile
# Run server
mix run --no-halt
# Run tests
mix test
# Interactive shell
iex -S mix
# Format code
mix format
```
## Error Handling
The server handles errors gracefully:
- **Protocol Errors**: Invalid JSON-RPC messages return appropriate error codes
- **Tool Errors**: Tool execution failures are returned as successful responses with `isError: true`
- **Timeouts**: Connections that don't initialize or remain inactive are automatically closed
- **Process Crashes**: Supervision tree ensures failed processes are restarted
## Advanced Usage
### Custom Validation
Implement custom tool validation by extending the built-in validation:
```elixir
def init_callback(session_id, init_params) do
# Custom logic based on session or client capabilities
if authorized?(session_id) do
{:ok, %{tools: get_tools_for_user(session_id), server_info: %{}}}
else
{:error, "Unauthorized"}
end
end
```
### Dynamic Tool Loading
Tools can be loaded dynamically based on client needs:
```elixir
def init_callback(_session_id, %{"clientInfo" => client_info}) do
tools = case client_info["name"] do
"development-client" -> development_tools()
"production-client" -> production_tools()
_ -> standard_tools()
end
{:ok, %{tools: tools, server_info: %{}}}
end
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests
5. Run `mix test` and `mix format`
6. Submit a pull request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Links
- [Model Context Protocol Specification](https://spec.modelcontextprotocol.io/)
- [MCP Official Documentation](https://modelcontextprotocol.io/)
- [Elixir Documentation](https://hexdocs.pm/elixir/)