This library provides a simple implementation of the Model Context Protocol (MCP) over Server-Sent Events (SSE).
For more information about the Model Context Protocol, visit: Model Context Protocol Documentation.
- Full MCP server implementation
- SSE connection management
- JSON-RPC message handling
- Tool registration and execution
- Session management
- Automatic ping/keepalive
- Error handling and validation
You must implement the MCPServer behaviour.
You only need to implement the required callbacks (handle_ping/1 and handle_initialize/2) and any optional callbacks for features you want to support.
The use MCPServer macro provides:
- Built-in message routing
- Protocol version validation
- Default implementations for optional callbacks
- JSON-RPC error handling
- Logging
See DefaultServer for a default implementation of the MCPServer behaviour.
- Add the required configuration to
config/config.exs:
# Configure MIME types for SSEconfig:mime,:types,%{"text/event-stream"=>["sse"]}# Configure the MCP Serverconfig:mcp_sse,:mcp_server,YourApp.YourMCPServer- Add to your dependencies in
mix.exs:
defdepsdo[{:mcp_sse,"~> 0.1.6"}]end- Configure your router (
lib/your_app_web/router.ex):
pipeline:ssedoplug:accepts,["sse"]endscope"/"dopipe_through:sseget"/sse",SSE.ConnectionPlug,:callpost"/message",SSE.ConnectionPlug,:callend- Run your application:
mix phx.server- Create a new Plug application with supervision:
mix new your_app --sup- Add the required configuration to
config/config.exs:
importConfig# Configure MIME types for SSEconfig:mime,:types,%{"text/event-stream"=>["sse"]}# Configure the MCP Serverconfig:mcp_sse,:mcp_server,YourApp.YourMCPServer- Add dependencies to
mix.exs:
defdepsdo[{:mcp_sse,"~> 0.1.6"},{:plug,"~> 1.14"},{:bandit,"~> 1.2"}]end- Configure your router (
lib/your_app/router.ex):
defmoduleYourApp.RouterdousePlug.RouterplugPlug.Parsers,parsers: [:urlencoded,:json],pass: ["text/*"],json_decoder: JSONplug:matchplug:ensure_session_idplug:dispatch# Middleware to ensure session ID existsdefensure_session_id(conn,_opts)docaseget_session_id(conn)donil-># Generate a new session ID if none existssession_id=generate_session_id()%{conn|query_params: Map.put(conn.query_params,"sessionId",session_id)}_session_id->connendend# Helper to get session ID from query paramsdefpget_session_id(conn)doconn.query_params["sessionId"]end# Generate a unique session IDdefpgenerate_session_iddoBase.encode16(:crypto.strong_rand_bytes(8),case: :lower)endforward"/sse",to: SSE.ConnectionPlugforward"/message",to: SSE.ConnectionPlugmatch_dosend_resp(conn,404,"Not found")endend- Set up your application supervision (
lib/your_app/application.ex):
defmoduleYourApp.ApplicationdouseApplication@impltruedefstart(_type,_args)dochildren=[{Bandit,plug: YourApp.Router,port: 4000}]opts=[strategy: :one_for_one,name: YourApp.Supervisor]Supervisor.start_link(children,opts)endend- Run your application:
mix run --no-halt- Start the inspector:
MCP_SERVER_URL=localhost:4000 npx @modelcontextprotocol/inspector@latest- Navigate to http://localhost:6274/
- Make sure your server is running
- Click
Connect - You can now list tools and call them
- Open Cursor Settings
- Navigate to the MCP tab
- Click
Add new global MCP server - Fill in the
~/.cursor/mcp.jsonwith:
{
"mcpServers": {
"your-mcp-server": {
"url": "http://localhost:4000/sse"
}
}
}- Make sure your server is running
- Ask Cursor to run one of your tools
The Bandit server can be configured with additional options in your application module:
# Example with custom port and HTTPSchildren=[{Bandit,plug: YourApp.Router,port: System.get_env("PORT","4000")|>String.to_integer(),scheme: :https,certfile: "priv/cert/selfsigned.pem",keyfile: "priv/cert/selfsigned_key.pem"}]You can customize the paths used for the SSE and message endpoints:
config:mcp_sse,sse_path: "/mcp/sse",# Default: "/sse"message_path: "/mcp/msg"# Default: "/message"This allows you to use custom paths in your routers:
# Phoenixscope"/mcp"dopipe_through:sseget"/sse",SSE.ConnectionPlug,:callpost"/msg",SSE.ConnectionPlug,:callend# Plugforward"/mcp/sse",to: SSE.ConnectionPlugforward"/mcp/msg",to: SSE.ConnectionPlugThe SSE connection sends periodic keepalive pings to prevent connection timeouts.
You can configure the ping interval or disable it entirely in config/config.exs:
# Set custom ping interval (in milliseconds)config:mcp_sse,:sse_keepalive_timeout,30_000# 30 seconds# Or disable pings entirelyconfig:mcp_sse,:sse_keepalive_timeout,:infinityTo see the MCP server in action:
- Start a server in one terminal:
# Our example server
elixir dev/example_server.exs
# Your Phoenix application
mix phx.server
# Your Plug application
mix run --no-halt- In another terminal, run the demo client script:
elixir dev/example_client.exsThe client script will:
- Connect to the SSE endpoint
- Initialize the connection
- List available tools
- Call the upcase tool with example input
- Display the results of each step
This provides a practical demonstration of the Model Context Protocol flow and server capabilities.
// Connect to SSE endpointconstsse=newEventSource('/sse');// Handle endpoint messagesse.addEventListener('endpoint',(e)=>{constmessageEndpoint=e.data;// Use messageEndpoint for subsequent JSON-RPC requests});// Send initialize requestfetch('/message?sessionId=YOUR_SESSION_ID',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({jsonrpc: '2.0',id: 1,method: 'initialize',params: {protocolVersion: '2024-11-05',capabilities: {}}})});The MCP SSE server requires a session ID for each connection. The router automatically:
- Uses an existing session ID from query parameters if provided
- Generates a new session ID if none exists
- Ensures all requests to
/sseand/messageendpoints have a valid session ID
When implementing tool responses in your MCP server, the content must follow the MCP specification for content types. The response content should be formatted as one of these types:
# Text content{:ok,%{jsonrpc: "2.0",id: request_id,result: %{content: [%{type: "text",text: "Your text response here"}]}}}# Image content{:ok,%{jsonrpc: "2.0",id: request_id,result: %{content: [%{type: "image",data: "base64_encoded_image_data",mimeType: "image/png"}]}}}# Resource reference{:ok,%{jsonrpc: "2.0",id: request_id,result: %{content: [%{type: "resource",resource: %{name: "resource_name",description: "resource description"}}]}}}For structured data like JSON, you should convert it to a formatted string:
defhandle_call_tool(request_id,%{"name"=>"list_companies"}=_params)docompanies=fetch_companies()# Your data fetching logic{:ok,%{jsonrpc: "2.0",id: request_id,result: %{content: [%{type: "text",text: JSON.encode!(companies,pretty: true)}]}}}endFor more details on response formatting, see the MCP Content Types Specification.
- Fork the repository and clone it
- Create a new branch in your fork
- Make your changes and commit them
- Push the changes to your fork
- Open a pull request in upstream