Packages
Official-style Elixir SDK for the Model Context Protocol (MCP) — client and server with stdio and Streamable HTTP transports.
Current section
Files
Jump to
Current section
Files
mcp_elixir_sdk
usage-rules.md
usage-rules.md
# MCP Elixir SDK — Usage Rules
> Condensed rules for AI agents and developers using `mcp_elixir_sdk`.
> For full documentation see [hexdocs](https://hexdocs.pm/mcp_elixir_sdk).
## What This Package Does
`mcp_elixir_sdk` is an Elixir SDK for the [Model Context Protocol](https://modelcontextprotocol.io/) (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
```elixir
# 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`:
```elixir
{: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:
```elixir
{:ok, info} = MCP.Client.connect(client)
# info contains server_info, capabilities, protocol_version, instructions
```
Call `MCP.Client.close/1` when done.
### Available Client Operations
| Function | Purpose |
|----------|---------|
| `connect/1` | Initialize handshake (required first) |
| `list_tools/1,2` | List server tools (with optional cursor) |
| `call_tool/3,4` | Call a tool by name with arguments |
| `list_resources/1,2` | List server resources |
| `read_resource/2` | Read a resource by URI |
| `list_prompts/1,2` | List server prompts |
| `get_prompt/3` | Get a prompt by name with arguments |
| `list_all_tools/1` | Auto-paginate all tools |
| `list_all_resources/1` | Auto-paginate all resources |
| `list_all_prompts/1` | Auto-paginate all prompts |
| `close/1` | Disconnect and clean up |
### Client Feature Callbacks
To support server-initiated requests, pass callback options to `start_link/1`:
```elixir
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.
```elixir
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
| Callback | Arity | Return |
|----------|-------|--------|
| `init/1` | 1 | `{:ok, state}` |
| `handle_list_tools/2` | 2 | `{:ok, tools, next_cursor, state}` |
| `handle_call_tool/3` | 3 | `{:ok, content, state}` or `{:error, code, message, state}` |
| `handle_call_tool/4` | 4 | Same as /3, but receives `ToolContext` for async ops |
| `handle_list_resources/2` | 2 | `{:ok, resources, next_cursor, state}` |
| `handle_read_resource/2` | 2 | `{:ok, contents, state}` or `{:error, code, message, state}` |
| `handle_list_prompts/2` | 2 | `{:ok, prompts, next_cursor, state}` |
| `handle_get_prompt/3` | 3 | `{:ok, result, state}` or `{:error, code, message, state}` |
| `handle_complete/3` | 3 | `{:ok, completion, state}` |
| `handle_set_log_level/2` | 2 | `{:ok, state}` |
| `handle_subscribe/2` | 2 | `{:ok, state}` or `{:error, code, message, state}` |
| `handle_unsubscribe/2` | 2 | `{:ok, state}` or `{:error, code, message, state}` |
| `handle_list_resource_templates/2` | 2 | `{:ok, templates, next_cursor, state}` |
### Starting a Server
**Stdio:**
```elixir
{: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):**
```elixir
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:
```elixir
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
| Code | Meaning |
|------|---------|
| `-32700` | Parse error |
| `-32600` | Invalid request |
| `-32601` | Method not found |
| `-32602` | Invalid params |
| `-32603` | Internal error |
| `-32002` | Resource not found (MCP-specific) |
## Module Reference
| Module | Purpose |
|--------|---------|
| `MCP.Client` | High-level client API |
| `MCP.Server` | High-level server API |
| `MCP.Server.Handler` | Server handler behaviour |
| `MCP.Server.ToolContext` | Async tool execution context |
| `MCP.Transport` | Transport behaviour |
| `MCP.Transport.Stdio` | Stdio transport |
| `MCP.Transport.StreamableHTTP.Client` | HTTP client transport |
| `MCP.Transport.StreamableHTTP.Plug` | HTTP server transport (Plug) |
| `MCP.Protocol` | JSON-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 |