Version: 0.11.0 | Last Updated: 2026-02-06
Table of Contents
- What is MCP (Model Context Protocol)
- MCP Server Types
- Creating Tools with deftool Macro
- Tool Schema (JSON Schema Format)
- Tool Execution and Return Values
- Creating SDK MCP Servers
- Tool Naming Convention
- Using External MCP Servers
- Combining MCP with Hooks and Permissions
- Best Practices
What is MCP (Model Context Protocol)
MCP (Model Context Protocol) is an open protocol that enables integration between LLM applications and external tools/data sources. It uses JSON-RPC 2.0 for communication and provides a standardized way to extend Claude's capabilities with custom tools.
Core Concepts
| Term | Description |
|---|---|
| MCP Server | A provider of tools, resources, or prompts |
| Tool | A function/capability that Claude can invoke |
| Resource | Data or context that Claude can access |
| Host | The LLM application (Claude Agent SDK) |
| Client | The MCP client that connects host to server |
Protocol Notes (Python Parity)
- SDK MCP routing implements
initialize,tools/list,tools/call, andnotifications/initialized. resources/listandprompts/listreturn JSON-RPC method-not-found errors (matching the Python SDK).- Tool names are kept as strings end-to-end; the registry normalizes tool names to strings to avoid atom leakage.
Why Use MCP?
- Standardized Protocol: Works across different LLM applications
- In-Process Execution: SDK MCP tools run without subprocess overhead
- Type Safety: JSON Schema validation for tool inputs
- Lifecycle Hooks: Integrate with the SDK's hook system for validation/logging
- Security: Fine-grained permission control over tool execution
MCP Server Types
The Claude Agent SDK supports two types of MCP servers:
1. SDK MCP Servers (In-Process)
Run directly within your Elixir application with no subprocess overhead.
# Define tools inline and execute them in your BEAM VM
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "my-tools",
version: "1.0.0",
tools: [MyTools.Calculator, MyTools.DateHelper]
)Benefits:
- Zero subprocess overhead
- Direct access to your application state
- Native Elixir error handling
- Hot code reloading support
2. External MCP Servers (Subprocess)
Traditional MCP servers running as separate processes via stdio transport.
# Use existing MCP server packages
external_server = %{
type: :stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
}Benefits:
- Use existing MCP server ecosystem
- Language-agnostic (Node.js, Python, etc.)
- Process isolation
Comparison
| Feature | SDK MCP | External MCP |
|---|---|---|
| Overhead | Minimal | Subprocess spawn |
| State Access | Direct | IPC required |
| Language | Elixir only | Any |
| Hot Reload | Yes | No |
| Error Handling | Native | JSON-RPC |
| Existing Ecosystem | Build your own | NPM packages |
Creating Tools with deftool Macro
The deftool macro provides a clean DSL for defining MCP tools.
Basic Syntax
defmodule MyTools do
use ClaudeAgentSDK.Tool
deftool :tool_name, "Tool description", %{schema} do
def execute(input) do
# Your implementation
{:ok, %{"content" => [%{"type" => "text", "text" => "result"}]}}
end
end
endComplete Example
defmodule Calculator do
use ClaudeAgentSDK.Tool
deftool :add, "Add two numbers together", %{
type: "object",
properties: %{
a: %{type: "number", description: "First number to add"},
b: %{type: "number", description: "Second number to add"}
},
required: ["a", "b"]
} do
def execute(%{"a" => a, "b" => b}) do
result = a + b
{:ok, %{"content" => [%{"type" => "text", "text" => "#{a} + #{b} = #{result}"}]}}
end
end
deftool :multiply, "Multiply two numbers", %{
type: "object",
properties: %{
a: %{type: "number", description: "First number"},
b: %{type: "number", description: "Second number"}
},
required: ["a", "b"]
} do
def execute(%{"a" => a, "b" => b}) do
result = a * b
{:ok, %{"content" => [%{"type" => "text", "text" => "#{a} * #{b} = #{result}"}]}}
end
end
deftool :factorial, "Calculate factorial of a number", %{
type: "object",
properties: %{
n: %{type: "integer", description: "Non-negative integer", minimum: 0, maximum: 20}
},
required: ["n"]
} do
def execute(%{"n" => n}) when n >= 0 do
result = factorial_calc(n)
{:ok, %{"content" => [%{"type" => "text", "text" => "#{n}! = #{result}"}]}}
end
def execute(%{"n" => n}) do
{:error, "Invalid input: #{n} must be non-negative"}
end
defp factorial_calc(0), do: 1
defp factorial_calc(n), do: n * factorial_calc(n - 1)
end
endTool Annotations
The deftool macro accepts a 5th argument with options, including :annotations for MCP tool annotations. Annotations are metadata hints for the client about tool behavior:
deftool :read_file, "Read a file from disk", %{
type: "object",
properties: %{
path: %{type: "string", description: "File path to read"}
},
required: ["path"]
},
annotations: %{
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
title: "Read File"
} do
def execute(%{"path" => path}) do
case File.read(path) do
{:ok, content} ->
{:ok, %{"content" => [%{"type" => "text", "text" => content}]}}
{:error, reason} ->
{:error, "Failed to read #{path}: #{inspect(reason)}"}
end
end
endStandard annotation fields:
| Annotation | Type | Description |
|---|---|---|
title | string | Human-readable display name |
readOnlyHint | boolean | Tool does not modify state |
destructiveHint | boolean | Tool may perform destructive operations |
idempotentHint | boolean | Repeated calls produce the same result |
openWorldHint | boolean | Tool interacts with external entities |
Annotations are included in tools/list responses and help clients make informed decisions about tool execution.
What deftool Generates
The macro creates a nested module for each tool:
# deftool :add, ... generates:
defmodule Calculator.Add do
@name :add
@description "Add two numbers together"
@schema %{...}
def name, do: @name
def description, do: @description
def schema, do: @schema
# Your execute/1 implementation
def execute(%{"a" => a, "b" => b}), do: ...
endTool Schema (JSON Schema Format)
Tool schemas follow JSON Schema draft-07 format with some MCP-specific conventions.
Simple Schema Helper
For common tool patterns, use the simple_schema/1 helper to reduce boilerplate:
alias ClaudeAgentSDK.Tool
# List of atoms - all string properties, all required
Tool.simple_schema([:name, :path])
# => %{type: "object", properties: %{name: %{type: "string"}, path: %{type: "string"}}, required: ["name", "path"]}
# Keyword list with types
Tool.simple_schema(name: :string, count: :number, enabled: :boolean)
# With descriptions
Tool.simple_schema(
name: {:string, "User's full name"},
age: {:number, "Age in years"}
)
# Optional fields
Tool.simple_schema(
name: :string,
email: {:string, optional: true}
)Supported types: :string, :number, :integer, :boolean, :array, :object
Example in deftool:
defmodule Calculator do
use ClaudeAgentSDK.Tool
alias ClaudeAgentSDK.Tool
deftool :add,
"Add two numbers",
Tool.simple_schema(
a: {:number, "First number"},
b: {:number, "Second number"}
) do
def execute(%{"a" => a, "b" => b}) do
{:ok, %{"content" => [%{"type" => "text", "text" => "#{a} + #{b} = #{a + b}"}]}}
end
end
endSchema Structure
%{
type: "object", # Always "object" for tool inputs
properties: %{ # Define each input parameter
param_name: %{
type: "string", # Type: string, number, integer, boolean, array, object
description: "...", # Human-readable description (shown to Claude)
enum: ["a", "b"], # Optional: allowed values
default: "value" # Optional: default value
}
},
required: ["param_name"] # List of required parameters
}Common Property Types
String
%{
type: "string",
description: "A text value",
minLength: 1,
maxLength: 1000,
pattern: "^[a-z]+$" # Regex pattern
}Number / Integer
%{
type: "number", # or "integer"
description: "A numeric value",
minimum: 0,
maximum: 100,
exclusiveMinimum: 0,
exclusiveMaximum: 100
}Boolean
%{
type: "boolean",
description: "True or false",
default: false
}Array
%{
type: "array",
description: "List of items",
items: %{type: "string"}, # Type of array elements
minItems: 1,
maxItems: 10,
uniqueItems: true
}Enum
%{
type: "string",
description: "One of the allowed values",
enum: ["option1", "option2", "option3"]
}Nested Object
%{
type: "object",
properties: %{
config: %{
type: "object",
properties: %{
enabled: %{type: "boolean"},
timeout: %{type: "integer"}
}
}
}
}Complete Schema Example
deftool :search_files, "Search for files matching criteria", %{
type: "object",
properties: %{
directory: %{
type: "string",
description: "Directory to search in"
},
pattern: %{
type: "string",
description: "Glob pattern to match files",
default: "*"
},
recursive: %{
type: "boolean",
description: "Search recursively",
default: true
},
file_types: %{
type: "array",
description: "File extensions to include",
items: %{type: "string"},
default: []
},
max_results: %{
type: "integer",
description: "Maximum number of results",
minimum: 1,
maximum: 1000,
default: 100
}
},
required: ["directory"]
} do
def execute(input) do
# Implementation
end
endTool Execution and Return Values
Success Return Format
Tools must return a tuple with content blocks:
{:ok, %{
"content" => [
%{"type" => "text", "text" => "Your result here"}
]
}}Multiple Content Blocks
{:ok, %{
"content" => [
%{"type" => "text", "text" => "Primary result"},
%{"type" => "text", "text" => "Additional information"}
]
}}Error Return Format
For handled errors:
{:error, "Error description"}For errors that should be visible to Claude:
{:ok, %{
"content" => [
%{"type" => "text", "text" => "Error: Invalid input"}
],
"is_error" => true
}}Return Value Examples
Simple Text Result
def execute(%{"query" => query}) do
result = perform_search(query)
{:ok, %{"content" => [%{"type" => "text", "text" => result}]}}
endJSON Result
def execute(%{"id" => id}) do
data = fetch_data(id)
json = Jason.encode!(data, pretty: true)
{:ok, %{"content" => [%{"type" => "text", "text" => json}]}}
endStructured Result with Metadata
def execute(input) do
{result, metadata} = process(input)
content = """
## Result
#{result}
## Metadata
- Duration: #{metadata.duration_ms}ms
- Items processed: #{metadata.count}
"""
{:ok, %{"content" => [%{"type" => "text", "text" => content}]}}
endError Handling
def execute(%{"file_path" => path}) do
case File.read(path) do
{:ok, content} ->
{:ok, %{"content" => [%{"type" => "text", "text" => content}]}}
{:error, :enoent} ->
{:error, "File not found: #{path}"}
{:error, :eacces} ->
{:ok, %{
"content" => [%{"type" => "text", "text" => "Permission denied: #{path}"}],
"is_error" => true
}}
{:error, reason} ->
{:error, "Failed to read file: #{inspect(reason)}"}
end
endCreating SDK MCP Servers
Use ClaudeAgentSDK.create_sdk_mcp_server/1 to create in-process MCP servers.
Basic Usage
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "my-tools",
version: "1.0.0",
tools: [MyTools.Add, MyTools.Multiply]
)Server Configuration
| Option | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Unique server identifier |
version | String | No | Server version (defaults to 1.0.0) |
tools | List | Yes | List of tool modules |
supervisor | pid/name | No | DynamicSupervisor to start the registry under |
Server Structure
The returned server map contains:
%{
type: :sdk, # Identifies as SDK MCP server
name: "my-tools", # Server name
version: "1.0.0", # Server version
registry_pid: #PID<0.123.0> # Tool registry process
}Direct registry calls use string tool names:
{:ok, result} =
ClaudeAgentSDK.Tool.Registry.execute_tool(server.registry_pid, "add", %{"a" => 1, "b" => 2})Using with Options
defmodule MathServer do
def create do
ClaudeAgentSDK.create_sdk_mcp_server(
name: "math",
version: "1.0.0",
tools: [Calculator.Add, Calculator.Multiply, Calculator.Factorial]
)
end
end
# In your query
server = MathServer.create()
options = %ClaudeAgentSDK.Options{
mcp_servers: %{"math" => server},
allowed_tools: ["mcp__math__add", "mcp__math__multiply", "mcp__math__factorial"],
permission_mode: :bypass_permissions
}
ClaudeAgentSDK.query("What is 15 + 27, then multiply by 3?", options)
|> Enum.to_list()You can also pass a JSON string or file path via mcp_servers (alias for mcp_config):
options = %Options{mcp_servers: "/path/to/mcp.json"}Complete Example
defmodule MyApp.Tools.DateTime do
use ClaudeAgentSDK.Tool
deftool :current_time, "Get current time in specified timezone", %{
type: "object",
properties: %{
timezone: %{
type: "string",
description: "Timezone (e.g., 'UTC', 'America/New_York')",
default: "UTC"
},
format: %{
type: "string",
description: "Output format",
enum: ["iso8601", "human", "unix"],
default: "iso8601"
}
},
required: []
} do
def execute(%{"timezone" => tz, "format" => format}) do
now = DateTime.utc_now()
result = case format do
"iso8601" -> DateTime.to_iso8601(now)
"human" -> Calendar.strftime(now, "%B %d, %Y at %H:%M:%S")
"unix" -> DateTime.to_unix(now) |> to_string()
end
{:ok, %{"content" => [%{"type" => "text", "text" => "Current time (#{tz}): #{result}"}]}}
end
def execute(input) do
execute(Map.merge(%{"timezone" => "UTC", "format" => "iso8601"}, input))
end
end
end
defmodule MyApp.ToolServer do
alias MyApp.Tools.DateTime
def start do
ClaudeAgentSDK.create_sdk_mcp_server(
name: "datetime",
version: "1.0.0",
tools: [DateTime.CurrentTime]
)
end
end
# Usage
server = MyApp.ToolServer.start()
options = %ClaudeAgentSDK.Options{
mcp_servers: %{"datetime" => server},
allowed_tools: ["mcp__datetime__current_time"]
}
ClaudeAgentSDK.query("What time is it?", options)Tool Naming Convention
MCP tools follow a strict naming convention: mcp__<server>__<tool>
Format
mcp__<server_name>__<tool_name>
^ ^
| |
| +-- Double underscore separator
+-- Prefix for MCP toolsExamples
| Server Name | Tool Name | Full Tool Name |
|---|---|---|
calculator | add | mcp__calculator__add |
math-tools | multiply | mcp__math-tools__multiply |
my_server | do_something | mcp__my_server__do_something |
Using Tool Names
In allowed_tools
options = %Options{
mcp_servers: %{"calc" => server},
allowed_tools: [
"mcp__calc__add",
"mcp__calc__multiply",
"mcp__calc__divide"
]
}In Hooks
alias ClaudeAgentSDK.Hooks.{Matcher, Output}
hooks = %{
pre_tool_use: [
# Match specific MCP tool
Matcher.new("mcp__calc__add", [&log_add/3]),
# Match all tools from a server (regex)
Matcher.new("mcp__calc__.*", [&audit_calc/3]),
# Match all MCP tools
Matcher.new("mcp__.*", [&log_mcp_usage/3])
]
}Tool Name in Hook Input
def my_hook(input, _tool_use_id, _context) do
case input["tool_name"] do
"mcp__calc__add" -> handle_add(input)
"mcp__calc__multiply" -> handle_multiply(input)
_ -> Output.allow()
end
endUsing External MCP Servers
External MCP servers run as separate processes and communicate via stdio transport.
Basic Configuration
external_server = %{
type: :stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
}
options = %ClaudeAgentSDK.Options{
mcp_servers: %{"filesystem" => external_server}
}Server Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
type | :stdio | Yes | Transport type (currently only stdio) |
command | String | Yes | Command to execute |
args | List | No | Command arguments |
env | Map | No | Environment variables |
Common External Servers
Filesystem Server
%{
type: :stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
}GitHub Server
%{
type: :stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: %{"GITHUB_TOKEN" => System.get_env("GITHUB_TOKEN")}
}Custom Server
%{
type: :stdio,
command: "python",
args: ["-m", "my_mcp_server"],
env: %{"CONFIG_PATH" => "/etc/my_server/config.json"}
}Combining SDK and External Servers
# SDK MCP server
calc_server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "calc",
version: "1.0.0",
tools: [Calculator.Add, Calculator.Multiply]
)
# External MCP server
fs_server = %{
type: :stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp/workspace"]
}
options = %ClaudeAgentSDK.Options{
mcp_servers: %{
"calc" => calc_server,
"filesystem" => fs_server
},
allowed_tools: [
"mcp__calc__add",
"mcp__calc__multiply",
"mcp__filesystem__read_file",
"mcp__filesystem__write_file"
]
}Combining MCP with Hooks and Permissions
MCP tools integrate seamlessly with the SDK's hook and permission systems.
Pre-Tool Hooks for MCP
alias ClaudeAgentSDK.Hooks.{Matcher, Output}
def validate_mcp_tool(input, _tool_use_id, _context) do
case input do
%{"tool_name" => "mcp__calc__" <> _op, "tool_input" => params} ->
# Validate numeric inputs
if valid_numbers?(params) do
Output.allow()
else
Output.deny("Invalid numeric input")
end
_ ->
Output.allow()
end
end
hooks = %{
pre_tool_use: [
Matcher.new("mcp__calc__.*", [&validate_mcp_tool/3])
]
}Post-Tool Hooks for Auditing
def audit_mcp_usage(input, tool_use_id, _context) do
Logger.info("MCP Tool Used",
tool: input["tool_name"],
tool_use_id: tool_use_id,
input: input["tool_input"],
result: input["tool_response"]
)
%{} # Don't modify behavior
end
hooks = %{
post_tool_use: [
Matcher.new("mcp__.*", [&audit_mcp_usage/3])
]
}Permission Callbacks
alias ClaudeAgentSDK.Permission.Result
permission_callback = fn context ->
case context.tool_name do
"mcp__filesystem__write_file" ->
# Only allow writes to specific directory
path = context.tool_input["path"]
if String.starts_with?(path, "/tmp/workspace/") do
Result.allow()
else
Result.deny("Writes only allowed in /tmp/workspace/")
end
"mcp__calc__" <> _ ->
# Always allow calculator tools
Result.allow()
_ ->
Result.allow()
end
end
options = %ClaudeAgentSDK.Options{
mcp_servers: %{"calc" => calc_server, "filesystem" => fs_server},
can_use_tool: permission_callback,
permission_mode: :default
}Complete Integration Example
defmodule MyApp.SecureMCPSetup do
alias ClaudeAgentSDK.{Options, Permission.Result}
alias ClaudeAgentSDK.Hooks.{Matcher, Output}
def build_options do
calc_server = build_calc_server()
hooks = build_hooks()
permission_callback = build_permission_callback()
%Options{
mcp_servers: %{"calc" => calc_server},
allowed_tools: ["mcp__calc__add", "mcp__calc__multiply"],
hooks: hooks,
can_use_tool: permission_callback,
permission_mode: :default
}
end
defp build_calc_server do
ClaudeAgentSDK.create_sdk_mcp_server(
name: "calc",
version: "1.0.0",
tools: [Calculator.Add, Calculator.Multiply]
)
end
defp build_hooks do
%{
pre_tool_use: [
# Log all MCP tool invocations
Matcher.new("mcp__.*", [&log_invocation/3]),
# Validate calculator inputs
Matcher.new("mcp__calc__.*", [&validate_numbers/3])
],
post_tool_use: [
# Audit all MCP results
Matcher.new("mcp__.*", [&audit_result/3])
]
}
end
defp build_permission_callback do
fn context ->
# Add rate limiting for MCP tools
if rate_limit_exceeded?(context.tool_name) do
Result.deny("Rate limit exceeded")
else
Result.allow()
end
end
end
defp log_invocation(input, tool_use_id, _context) do
IO.puts("[MCP] Invoking #{input["tool_name"]} (#{tool_use_id})")
%{}
end
defp validate_numbers(%{"tool_input" => params}, _id, _ctx) do
if Enum.all?(Map.values(params), &is_number/1) do
Output.allow()
else
Output.deny("All inputs must be numbers")
end
end
defp validate_numbers(_, _, _), do: Output.allow()
defp audit_result(input, tool_use_id, _context) do
IO.puts("[MCP] Completed #{input["tool_name"]} (#{tool_use_id})")
%{}
end
defp rate_limit_exceeded?(_tool_name), do: false
endBest Practices
Tool Design
- Single Responsibility: Each tool should do one thing well
- Clear Descriptions: Write descriptions that help Claude understand when to use the tool
- Validate Inputs: Use JSON Schema constraints and runtime validation
- Handle Errors Gracefully: Return meaningful error messages
# Good: Clear, focused tool
deftool :get_user_by_id, "Fetch a user record by their unique ID", %{
type: "object",
properties: %{
user_id: %{type: "string", description: "Unique user identifier (UUID format)"}
},
required: ["user_id"]
} do
def execute(%{"user_id" => id}) do
case Users.get(id) do
{:ok, user} -> {:ok, %{"content" => [%{"type" => "text", "text" => format_user(user)}]}}
{:error, :not_found} -> {:error, "User not found: #{id}"}
end
end
end
# Bad: Too broad, unclear purpose
deftool :do_stuff, "Does things with data", %{...}Schema Design
- Use Descriptive Property Names: Self-documenting schemas
- Add Descriptions: Help Claude understand each parameter
- Set Constraints: Use min/max, patterns, enums
- Provide Defaults: For optional parameters
# Good: Well-documented schema
%{
type: "object",
properties: %{
query: %{
type: "string",
description: "Search query string",
minLength: 1,
maxLength: 500
},
limit: %{
type: "integer",
description: "Maximum results to return",
minimum: 1,
maximum: 100,
default: 10
},
sort_order: %{
type: "string",
description: "Result ordering",
enum: ["asc", "desc"],
default: "desc"
}
},
required: ["query"]
}Error Handling
- Pattern Match Inputs: Handle unexpected input gracefully
- Use is_error Flag: For errors Claude should know about
- Provide Context: Help Claude understand what went wrong
def execute(%{"file_path" => path}) do
case File.read(path) do
{:ok, content} ->
{:ok, %{"content" => [%{"type" => "text", "text" => content}]}}
{:error, :enoent} ->
{:ok, %{
"content" => [%{"type" => "text", "text" => "File not found: #{path}. Please check the path and try again."}],
"is_error" => true
}}
{:error, :eacces} ->
{:ok, %{
"content" => [%{"type" => "text", "text" => "Permission denied reading: #{path}"}],
"is_error" => true
}}
{:error, reason} ->
{:error, "Unexpected error reading #{path}: #{inspect(reason)}"}
end
end
# Always handle unmatched patterns
def execute(input) do
{:error, "Invalid input format: #{inspect(input)}"}
endSecurity
- Validate All Inputs: Never trust user/Claude input
- Use Hooks for Authorization: Integrate with your auth system
- Limit Scope: Only expose necessary functionality
- Audit Usage: Log tool invocations for monitoring
# Security hook example
def security_check(input, _tool_use_id, context) do
tool = input["tool_name"]
user = context["user_id"]
if authorized?(user, tool) do
Output.allow()
else
Logger.warn("Unauthorized tool access", tool: tool, user: user)
Output.deny("Not authorized to use #{tool}")
end
endPerformance
- Keep Tools Fast: Aim for < 100ms execution time
- Use Async for Slow Operations: Spawn tasks for long-running work
- Cache When Appropriate: Avoid redundant computations
- Set Timeouts: Prevent hanging operations
def execute(%{"url" => url}) do
# Use Task with timeout for external calls
task = Task.async(fn -> HTTPClient.get(url) end)
case Task.yield(task, 5000) || Task.shutdown(task) do
{:ok, {:ok, response}} ->
{:ok, %{"content" => [%{"type" => "text", "text" => response.body}]}}
{:ok, {:error, reason}} ->
{:error, "HTTP request failed: #{reason}"}
nil ->
{:error, "Request timed out after 5 seconds"}
end
endNaming Conventions
- Use snake_case for Tool Names:
calculate_total, notcalculateTotal - Use Descriptive Server Names:
user-management, notum - Group Related Tools: Single server with multiple related tools
# Good: Organized tool groupings
defmodule UserTools do
use ClaudeAgentSDK.Tool
deftool :get_user, "...", %{...}
deftool :create_user, "...", %{...}
deftool :update_user, "...", %{...}
deftool :delete_user, "...", %{...}
end
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "user-management",
version: "1.0.0",
tools: [UserTools.GetUser, UserTools.CreateUser, UserTools.UpdateUser, UserTools.DeleteUser]
)Examples
See these example files for working implementations:
examples/sdk_mcp_tools_live.exs- Basic SDK MCP toolsexamples/advanced_features/sdk_mcp_live_demo.exs- Comprehensive MCP demoexamples/streaming_tools/sdk_mcp_streaming.exs- Streaming with MCP tools
Quick Reference
Create a Tool
defmodule MyTool do
use ClaudeAgentSDK.Tool
deftool :name, "description", %{type: "object", properties: %{...}} do
def execute(input), do: {:ok, %{"content" => [%{"type" => "text", "text" => "result"}]}}
end
endCreate a Server
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "server-name",
version: "1.0.0",
tools: [MyTool.Name]
)Use in Query
options = %ClaudeAgentSDK.Options{
mcp_servers: %{"server-name" => server},
allowed_tools: ["mcp__server-name__name"]
}
ClaudeAgentSDK.query("prompt", options)Documentation
MCP Status API
Query the MCP server status at runtime:
{:ok, status} = ClaudeAgentSDK.Client.get_mcp_status(client)
IO.inspect(status, label: "MCP status")Async Tool Dispatch
SDK MCP tools/call requests are dispatched asynchronously via TaskSupervisor, so long-running tool execution no longer blocks the Client callback path. Configure the execution timeout via application config:
config :claude_agent_sdk, tool_execution_timeout_ms: 30_000Documentation
- Hooks Guide: hooks.md
- Configuration Guide: configuration.md
- Permissions Guide: permissions.md
Need Help?
iex> h ClaudeAgentSDK.Tool
iex> h ClaudeAgentSDK.create_sdk_mcp_server
iex> h ClaudeAgentSDK.Options