Behaviour for implementing stateless MCP servers.
An MCP server provides tools, resources, and prompts to LLM clients. Servers implement callbacks to handle client requests concurrently.
Changes in v0.4.0
The server is now fully stateless - just pure compiled functions:
- No GenServer, no Agent, no process overhead
- No supervision tree required
- Callbacks receive the Plug.Conn for request context
- Each HTTP request runs in parallel (limited only by Bandit's process pool)
Example (Using DSL - Recommended)
defmodule MyApp.MCPServer do
use ConduitMcp.Server
tool "echo", "Echo back the input" do
param :message, :string, "Message to echo", required: true
handle fn _conn, %{"message" => msg} ->
text(msg)
end
end
tool "calculate", "Perform calculations" do
param :operation, :string, "Math operation", enum: ~w(add sub mul div), required: true
param :a, :number, "First number", required: true
param :b, :number, "Second number", required: true
handle MyMath, :calculate
end
endExample (Manual - Advanced)
defmodule MyApp.MCPServer do
use ConduitMcp.Server, dsl: false # Disable DSL
@tools [
%{
"name" => "echo",
"description" => "Echo back the input",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"message" => %{"type" => "string", "description" => "Message to echo"}
},
"required" => ["message"]
}
}
]
@impl true
def handle_list_tools(_conn) do
{:ok, %{"tools" => @tools}}
end
@impl true
def handle_call_tool(_conn, "echo", %{"message" => msg}) do
{:ok, %{"content" => [%{"type" => "text", "text" => msg}]}}
end
endThen in your supervision tree, just pass the module:
{Bandit,
plug: {ConduitMcp.Transport.StreamableHTTP, server_module: MyApp.MCPServer},
port: 4001}Using Connection Context
The conn parameter allows access to request metadata:
def handle_call_tool(conn, "private_data", _params) do
# Check authentication
user_id = conn.assigns[:user_id]
# Access headers
auth_header = Plug.Conn.get_req_header(conn, "authorization")
{:ok, %{"content" => [%{"type" => "text", "text" => "Data for #{user_id}"}]}}
endMutable State
If you need mutable state, use external mechanisms:
# Option 1: ETS
def handle_call_tool(_conn, "increment", _params) do
:ets.update_counter(:my_counter, :count, 1)
count = :ets.lookup_element(:my_counter, :count, 2)
{:ok, %{"content" => [%{"type" => "text", "text" => "Count: #{count}"}]}}
end
# Option 2: Agent/GenServer
def handle_call_tool(_conn, "get_cache", %{"key" => key}) do
value = MyApp.Cache.get(key)
{:ok, %{"content" => [%{"type" => "text", "text" => value}]}}
end
Summary
Callbacks
Handle tool execution.
Handle autocompletion for prompt arguments or resource template parameters.
Handle getting a prompt.
Handle listing available prompts.
Handle listing available resources.
Handle listing available tools.
Handle reading a resource.
Handle setting the log level for server-to-client logging.
Handle subscribing to resource changes.
Handle unsubscribing from resource changes.
Types
Callbacks
@callback handle_call_tool(conn(), tool_name(), tool_params()) :: {:ok, result :: map()} | {:error, error :: map()}
Handle tool execution.
@callback handle_complete(conn(), ref :: map(), argument :: map()) :: {:ok, map()} | {:error, map()}
Handle autocompletion for prompt arguments or resource template parameters.
Receives the reference type (:prompt or :resource), the reference name,
the argument/parameter name, and the partial value typed so far.
Should return {:ok, %{"completion" => %{"values" => [...], "hasMore" => false}}}.
@callback handle_get_prompt(conn(), prompt_name(), prompt_args()) :: {:ok, messages :: map()} | {:error, error :: map()}
Handle getting a prompt.
Handle listing available prompts.
The arity-2 variant receives request params for pagination support.
@callback handle_list_resources(conn()) :: {:ok, %{optional(String.t()) => any()}} | {:error, map()}
Handle listing available resources.
The arity-2 variant receives request params for pagination support.
Handle listing available tools.
The arity-2 variant receives request params (e.g., %{"cursor" => "..."})
for pagination support. Implement arity-2 to support cursor-based pagination.
Handle reading a resource.
Handle setting the log level for server-to-client logging.
Receives the desired log level as a string (e.g., "debug", "info", "warning", "error").
Handle subscribing to resource changes.
Receives the resource URI to subscribe to.
Handle unsubscribing from resource changes.
Receives the resource URI to unsubscribe from.