MCP Elixir SDK — Usage Rules

Copy Markdown View Source

Condensed rules for AI agents and developers using mcp_elixir_sdk. For full documentation see hexdocs.

What This Package Does

mcp_elixir_sdk is an Elixir SDK for the Model Context Protocol (MCP) — an open protocol for integrating LLM applications with external data sources and tools. It provides both client and server implementations with pluggable transports. Protocol version: 2025-11-25.

Public modules live under the MCP.* namespace (e.g. MCP.Client, MCP.Server).

Installation

# mix.exs deps
{:mcp_elixir_sdk, "~> 1.0"}

# Also add these for Streamable HTTP transport:
{:req, "~> 0.5"}      # HTTP client
{:plug, "~> 1.16"}    # HTTP framework
{:bandit, "~> 1.5"}   # HTTP server

The stdio transport requires no additional dependencies.

Client Rules

Starting a Client

Always provide :transport and :client_info:

{:ok, client} = MCP.Client.start_link(
  transport: {MCP.Transport.Stdio, command: "mcp-server", args: []},
  client_info: %{name: "my_app", version: "1.0.0"}
)

Connection Lifecycle

You must call connect/1 before any other operation. It performs the MCP initialization handshake:

{:ok, info} = MCP.Client.connect(client)
# info contains server_info, capabilities, protocol_version, instructions

Call MCP.Client.close/1 when done.

Available Client Operations

FunctionPurpose
connect/1Initialize handshake (required first)
list_tools/1,2List server tools (with optional cursor)
call_tool/3,4Call a tool by name with arguments
list_resources/1,2List server resources
read_resource/2Read a resource by URI
list_prompts/1,2List server prompts
get_prompt/3Get a prompt by name with arguments
list_all_tools/1Auto-paginate all tools
list_all_resources/1Auto-paginate all resources
list_all_prompts/1Auto-paginate all prompts
close/1Disconnect and clean up

Client Feature Callbacks

To support server-initiated requests, pass callback options to start_link/1:

MCP.Client.start_link(
  transport: {transport_mod, transport_opts},
  client_info: %{name: "my_app", version: "1.0.0"},
  on_sampling: fn params -> {:ok, %{"role" => "assistant", "content" => %{"type" => "text", "text" => "response"}, "model" => "my-model", "stopReason" => "endTurn"}} end,
  on_roots_list: fn _params -> {:ok, %{"roots" => [%{"uri" => "file:///path", "name" => "Root"}]}} end,
  on_elicitation: fn params -> {:ok, %{"action" => "accept", "content" => %{}}} end,
  notification_handler: fn method, params -> IO.puts("#{method}") end
)

Capabilities are auto-advertised when callbacks are provided.

Server Rules

Implementing a Handler

Create a module with @behaviour MCP.Server.Handler. Only init/1 is required — implement additional callbacks to enable features. The server auto-detects capabilities based on which callbacks you implement.

defmodule MyHandler do
  @behaviour MCP.Server.Handler

  @impl true
  def init(_opts), do: {:ok, %{}}

  @impl true
  def handle_list_tools(_cursor, state) do
    tools = [
      %{
        "name" => "my_tool",
        "description" => "Does something",
        "inputSchema" => %{
          "type" => "object",
          "properties" => %{"arg" => %{"type" => "string"}},
          "required" => ["arg"]
        }
      }
    ]
    {:ok, tools, nil, state}
  end

  @impl true
  def handle_call_tool("my_tool", %{"arg" => arg}, state) do
    {:ok, [%{"type" => "text", "text" => "Result: #{arg}"}], state}
  end
end

Handler Callback Signatures

CallbackArityReturn
init/11{:ok, state}
handle_list_tools/22{:ok, tools, next_cursor, state}
handle_call_tool/33{:ok, content, state} or {:error, code, message, state}
handle_call_tool/44Same as /3, but receives ToolContext for async ops
handle_list_resources/22{:ok, resources, next_cursor, state}
handle_read_resource/22{:ok, contents, state} or {:error, code, message, state}
handle_list_prompts/22{:ok, prompts, next_cursor, state}
handle_get_prompt/33{:ok, result, state} or {:error, code, message, state}
handle_complete/33{:ok, completion, state}
handle_set_log_level/22{:ok, state}
handle_subscribe/22{:ok, state} or {:error, code, message, state}
handle_unsubscribe/22{:ok, state} or {:error, code, message, state}
handle_list_resource_templates/22{:ok, templates, next_cursor, state}

Starting a Server

Stdio:

{:ok, server} = MCP.Server.start_link(
  transport: {MCP.Transport.Stdio, mode: :server},
  handler: {MyHandler, []},
  server_info: %{name: "my_server", version: "1.0.0"}
)

Streamable HTTP (Plug + Bandit):

plug_config = MCP.Transport.StreamableHTTP.Plug.init(
  server_mod: MyHandler,
  server_opts: [server_info: %{name: "my_server", version: "1.0.0"}]
)

{:ok, _bandit} = Bandit.start_link(
  plug: {MCP.Transport.StreamableHTTP.Plug, plug_config},
  port: 8080,
  ip: {127, 0, 0, 1}
)

Async Tool Execution with ToolContext

Implement handle_call_tool/4 (4-arity) instead of /3 to get a ToolContext for sending log messages, progress updates, and making server-to-client requests:

def handle_call_tool("analyze", args, ctx, state) do
  ToolContext.log(ctx, "info", "Starting analysis")
  ToolContext.send_progress(ctx, 0, 100)

  {:ok, result} = ToolContext.request_sampling(ctx, %{
    "messages" => [%{"role" => "user", "content" => %{"type" => "text", "text" => "Analyze: #{args["code"]}"}}],
    "maxTokens" => 1000
  })

  ToolContext.send_progress(ctx, 100, 100)
  {:ok, [%{"type" => "text", "text" => result["content"]["text"]}], state}
end

ToolContext functions: log/3, send_progress/3, send_notification/3, request/4, request_sampling/2, request_elicitation/2.

Transport Rules

Stdio Transport

  • Client mode: {MCP.Transport.Stdio, command: "cmd", args: ["arg1"]}
  • Server mode: {MCP.Transport.Stdio, mode: :server}
  • Messages are newline-delimited JSON-RPC. Must NOT contain embedded newlines.
  • Works with zero additional dependencies.

Streamable HTTP Transport

  • Client: {MCP.Transport.StreamableHTTP.Client, url: "http://host:port/mcp"}
  • Server: Use MCP.Transport.StreamableHTTP.Plug with Bandit (see above)
  • Requires :req, :plug, and :bandit dependencies.
  • Uses POST for sending, GET for SSE listening, MCP-Session-Id header for stateful sessions.

Critical Gotchas

  1. Always call connect/1 first. No operations work before the initialization handshake completes (except ping).

  2. Sampling over HTTP times out. When using ToolContext.request_sampling/2 over Streamable HTTP, the client's Req.post blocks until the SSE stream completes, so the client cannot respond to the sampling request. The server's request_timeout (default 30s) returns {:error, :timeout}. Always handle the error case. Sampling works correctly over stdio.

  3. Content is always a list of maps. Tool results, resource contents, and prompt messages use [%{"type" => "text", "text" => "..."}] format. Never return a bare string.

  4. Tool input schemas use JSON Schema. The "inputSchema" field must be a valid JSON Schema object with "type" => "object".

  5. Cursors for pagination. List callbacks receive a cursor and must return {:ok, items, next_cursor, state}. Return nil for next_cursor when there are no more pages.

  6. Error tuples include state. All error returns are {:error, code, message, state} — don't forget to return the handler state.

  7. Capability auto-detection. The server only advertises capabilities for callbacks your handler implements. No need to configure capabilities manually.

  8. JSON-RPC 2.0 compliance. All messages are valid JSON-RPC 2.0. IDs are unique per session, never null.

Common Error Codes

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32002Resource not found (MCP-specific)

Module Reference

ModulePurpose
MCP.ClientHigh-level client API
MCP.ServerHigh-level server API
MCP.Server.HandlerServer handler behaviour
MCP.Server.ToolContextAsync tool execution context
MCP.TransportTransport behaviour
MCP.Transport.StdioStdio transport
MCP.Transport.StreamableHTTP.ClientHTTP client transport
MCP.Transport.StreamableHTTP.PlugHTTP server transport (Plug)
MCP.ProtocolJSON-RPC 2.0 framing and ID generation
MCP.Protocol.Types.*MCP type structs (Tool, Resource, Prompt, Content, etc.)
MCP.Protocol.Messages.*Request/response/notification structs
MCP.Protocol.Capabilities.*Client and server capability structs