CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-claude-agent-sdk

Python SDK for programmatically interacting with Claude Code, enabling AI-powered automation workflows with support for bidirectional conversations, custom in-process tools, hooks, and fine-grained permission control

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

The risk profile of this skill

This plugin was archived by the owner on Jun 3, 2026

Reason: Retiring all tiles created prior to the transition to plugin support

Overview
Eval results
Files

custom-tools.mddocs/

Custom Tools (In-Process MCP Servers)

Create custom tools as Python functions that Claude can invoke, running in-process without subprocess overhead. The SDK provides decorators and helpers for building Model Context Protocol (MCP) servers directly in your Python application.

Capabilities

Tool Decorator

Define custom tools using the @tool decorator.

def tool(
    name: str,
    description: str,
    input_schema: type | dict[str, Any]
) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:
    """
    Decorator for creating custom tools.

    Creates a tool that can be used with SDK MCP servers. The tool runs
    in-process within your Python application, providing better performance
    than external MCP servers.

    Args:
        name: Unique identifier for the tool. This is what Claude will use
            to reference the tool in function calls.
        description: Human-readable description of what the tool does.
            This helps Claude understand when to use the tool.
        input_schema: Schema defining the tool's input parameters. Can be:
            - A dictionary mapping parameter names to types (e.g., {"text": str})
            - A TypedDict class for more complex schemas
            - A JSON Schema dictionary for full validation

    Returns:
        A decorator function that wraps the tool implementation and returns
        an SdkMcpTool instance ready for use with create_sdk_mcp_server().

    Notes:
        - The tool function must be async (defined with async def)
        - The function receives a single dict argument with the input parameters
        - The function should return a dict with a "content" key containing the response
        - Errors can be indicated by including "is_error": True in the response
    """

Parameters:

  • name (str): Unique identifier for the tool. Claude uses this name to reference the tool in function calls.

  • description (str): Human-readable description helping Claude understand when and how to use the tool.

  • input_schema (type | dict[str, Any]): Schema defining input parameters. Options:

    • Simple dict: {"param_name": type} (e.g., {"text": str, "count": int})
    • TypedDict class for structured schemas
    • Full JSON Schema dict with validation rules

Returns:

Decorator that wraps the async handler function and returns an SdkMcpTool instance.

Usage Example - Basic Tool:

from claude_agent_sdk import tool

@tool("greet", "Greet a user by name", {"name": str})
async def greet(args):
    return {
        "content": [
            {"type": "text", "text": f"Hello, {args['name']}!"}
        ]
    }

Usage Example - Multiple Parameters:

@tool("add", "Add two numbers", {"a": float, "b": float})
async def add_numbers(args):
    result = args["a"] + args["b"]
    return {
        "content": [
            {"type": "text", "text": f"Result: {result}"}
        ]
    }

Usage Example - Error Handling:

@tool("divide", "Divide two numbers", {"a": float, "b": float})
async def divide(args):
    if args["b"] == 0:
        return {
            "content": [{"type": "text", "text": "Error: Division by zero"}],
            "is_error": True
        }
    result = args["a"] / args["b"]
    return {
        "content": [{"type": "text", "text": f"Result: {result}"}]
    }

Usage Example - Complex Schema:

from typing import TypedDict

class SearchInput(TypedDict):
    query: str
    limit: int
    filters: dict[str, str]

@tool("search", "Search database", SearchInput)
async def search_database(args):
    query = args["query"]
    limit = args.get("limit", 10)
    filters = args.get("filters", {})

    # Perform search
    results = await db.search(query, limit, filters)

    return {
        "content": [
            {"type": "text", "text": f"Found {len(results)} results"}
        ]
    }

Usage Example - JSON Schema:

schema = {
    "type": "object",
    "properties": {
        "text": {"type": "string", "minLength": 1},
        "count": {"type": "integer", "minimum": 0, "maximum": 100}
    },
    "required": ["text"]
}

@tool("repeat", "Repeat text multiple times", schema)
async def repeat_text(args):
    text = args["text"]
    count = args.get("count", 1)
    return {
        "content": [
            {"type": "text", "text": text * count}
        ]
    }

Create MCP Server

Create an in-process MCP server with custom tools.

def create_sdk_mcp_server(
    name: str,
    version: str = "1.0.0",
    tools: list[SdkMcpTool[Any]] | None = None
) -> McpSdkServerConfig:
    """
    Create an in-process MCP server that runs within your Python application.

    Unlike external MCP servers that run as separate processes, SDK MCP servers
    run directly in your application's process. This provides:
    - Better performance (no IPC overhead)
    - Simpler deployment (single process)
    - Easier debugging (same process)
    - Direct access to your application's state

    Args:
        name: Unique identifier for the server. This name is used to reference
            the server in the mcp_servers configuration.
        version: Server version string. Defaults to "1.0.0". This is for
            informational purposes and doesn't affect functionality.
        tools: List of SdkMcpTool instances created with the @tool decorator.
            These are the functions that Claude can call through this server.
            If None or empty, the server will have no tools (rarely useful).

    Returns:
        McpSdkServerConfig: A configuration object that can be passed to
        ClaudeAgentOptions.mcp_servers. This config contains the server
        instance and metadata needed for the SDK to route tool calls.

    Notes:
        - The server runs in the same process as your Python application
        - Tools have direct access to your application's variables and state
        - No subprocess or IPC overhead for tool calls
        - Server lifecycle is managed automatically by the SDK
    """

Parameters:

  • name (str): Unique identifier for the server used in mcp_servers configuration.

  • version (str): Server version string for informational purposes. Default: "1.0.0".

  • tools (list[SdkMcpTool[Any]] | None): List of tool instances created with @tool decorator.

Returns:

McpSdkServerConfig for use in ClaudeAgentOptions.mcp_servers.

Usage Example - Simple Server:

from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions

@tool("add", "Add numbers", {"a": float, "b": float})
async def add(args):
    return {"content": [{"type": "text", "text": f"Sum: {args['a'] + args['b']}"}]}

@tool("multiply", "Multiply numbers", {"a": float, "b": float})
async def multiply(args):
    return {"content": [{"type": "text", "text": f"Product: {args['a'] * args['b']}"}]}

# Create server
calculator = create_sdk_mcp_server(
    name="calculator",
    version="2.0.0",
    tools=[add, multiply]
)

# Use with Claude
options = ClaudeAgentOptions(
    mcp_servers={"calc": calculator},
    allowed_tools=["add", "multiply"]
)

Usage Example - Server with Application State:

from claude_agent_sdk import tool, create_sdk_mcp_server

class DataStore:
    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def get_items(self):
        return self.items

# Create store instance
store = DataStore()

# Tools have access to store
@tool("add_item", "Add item to store", {"item": str})
async def add_item(args):
    store.add_item(args["item"])
    return {"content": [{"type": "text", "text": f"Added: {args['item']}"}]}

@tool("list_items", "List all items", {})
async def list_items(args):
    items = store.get_items()
    return {"content": [{"type": "text", "text": f"Items: {items}"}]}

# Create server
server = create_sdk_mcp_server("store", tools=[add_item, list_items])

SdkMcpTool

Tool definition dataclass (typically created by @tool decorator).

@dataclass
class SdkMcpTool(Generic[T]):
    """Custom tool definition."""

    name: str
    description: str
    input_schema: type[T] | dict[str, Any]
    handler: Callable[[T], Awaitable[dict[str, Any]]]

Fields:

  • name (str): Tool name.

  • description (str): Tool description for Claude.

  • input_schema (type[T] | dict[str, Any]): Input schema as type or dict.

  • handler (Callable[[T], Awaitable[dict[str, Any]]]): Async handler function.

Note: Typically you create tools using the @tool decorator rather than instantiating SdkMcpTool directly.

Manual Usage Example:

from claude_agent_sdk import SdkMcpTool

async def my_handler(args):
    return {"content": [{"type": "text", "text": "Result"}]}

tool_instance = SdkMcpTool(
    name="my_tool",
    description="Does something",
    input_schema={"param": str},
    handler=my_handler
)

Complete Examples

Calculator Server

from claude_agent_sdk import (
    tool, create_sdk_mcp_server, query,
    ClaudeAgentOptions, AssistantMessage, TextBlock
)
import anyio

@tool("add", "Add two numbers", {"a": float, "b": float})
async def add(args):
    result = args["a"] + args["b"]
    return {"content": [{"type": "text", "text": f"{result}"}]}

@tool("subtract", "Subtract two numbers", {"a": float, "b": float})
async def subtract(args):
    result = args["a"] - args["b"]
    return {"content": [{"type": "text", "text": f"{result}"}]}

@tool("multiply", "Multiply two numbers", {"a": float, "b": float})
async def multiply(args):
    result = args["a"] * args["b"]
    return {"content": [{"type": "text", "text": f"{result}"}]}

@tool("divide", "Divide two numbers", {"a": float, "b": float})
async def divide(args):
    if args["b"] == 0:
        return {
            "content": [{"type": "text", "text": "Error: Division by zero"}],
            "is_error": True
        }
    result = args["a"] / args["b"]
    return {"content": [{"type": "text", "text": f"{result}"}]}

# Create calculator server
calc_server = create_sdk_mcp_server(
    name="calculator",
    version="1.0.0",
    tools=[add, subtract, multiply, divide]
)

# Use calculator
async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"calc": calc_server},
        allowed_tools=["add", "subtract", "multiply", "divide"]
    )

    async for msg in query(prompt="Calculate (42 * 17) + (100 / 4)", options=options):
        if isinstance(msg, AssistantMessage):
            for block in msg.content:
                if isinstance(block, TextBlock):
                    print(block.text)

anyio.run(main)

Database Server

from claude_agent_sdk import tool, create_sdk_mcp_server
from typing import TypedDict, Literal

class QueryInput(TypedDict):
    sql: str
    params: dict[str, Any]

class InsertInput(TypedDict):
    table: str
    data: dict[str, Any]

class Database:
    def __init__(self):
        self.tables = {}

    async def query(self, sql: str, params: dict):
        # Execute query
        pass

    async def insert(self, table: str, data: dict):
        # Insert data
        if table not in self.tables:
            self.tables[table] = []
        self.tables[table].append(data)

# Create database instance
db = Database()

@tool("db_query", "Execute SQL query", QueryInput)
async def db_query(args):
    result = await db.query(args["sql"], args["params"])
    return {"content": [{"type": "text", "text": f"Query result: {result}"}]}

@tool("db_insert", "Insert data into table", InsertInput)
async def db_insert(args):
    await db.insert(args["table"], args["data"])
    return {"content": [{"type": "text", "text": "Data inserted successfully"}]}

# Create server
db_server = create_sdk_mcp_server(
    name="database",
    version="1.0.0",
    tools=[db_query, db_insert]
)

File Processing Server

from claude_agent_sdk import tool, create_sdk_mcp_server
import hashlib

@tool("hash_file", "Calculate file hash", {"path": str, "algorithm": str})
async def hash_file(args):
    path = args["path"]
    algorithm = args.get("algorithm", "sha256")

    try:
        hasher = hashlib.new(algorithm)
        with open(path, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hasher.update(chunk)
        return {
            "content": [{"type": "text", "text": f"{algorithm}: {hasher.hexdigest()}"}]
        }
    except Exception as e:
        return {
            "content": [{"type": "text", "text": f"Error: {e}"}],
            "is_error": True
        }

@tool("file_stats", "Get file statistics", {"path": str})
async def file_stats(args):
    import os
    try:
        stats = os.stat(args["path"])
        return {
            "content": [{
                "type": "text",
                "text": f"Size: {stats.st_size} bytes, Modified: {stats.st_mtime}"
            }]
        }
    except Exception as e:
        return {
            "content": [{"type": "text", "text": f"Error: {e}"}],
            "is_error": True
        }

# Create server
file_server = create_sdk_mcp_server(
    name="file_tools",
    version="1.0.0",
    tools=[hash_file, file_stats]
)

API Client Server

from claude_agent_sdk import tool, create_sdk_mcp_server
import httpx

class APIClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key

    async def get(self, endpoint: str):
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self.base_url}{endpoint}",
                headers={"Authorization": f"Bearer {self.api_key}"}
            )
            return response.json()

    async def post(self, endpoint: str, data: dict):
        async with httpx.AsyncClient() as client:
            response = await client.post(
                f"{self.base_url}{endpoint}",
                json=data,
                headers={"Authorization": f"Bearer {self.api_key}"}
            )
            return response.json()

# Create API client
api = APIClient("https://api.example.com", "secret-key")

@tool("api_get", "Fetch data from API", {"endpoint": str})
async def api_get(args):
    try:
        data = await api.get(args["endpoint"])
        return {"content": [{"type": "text", "text": str(data)}]}
    except Exception as e:
        return {
            "content": [{"type": "text", "text": f"API Error: {e}"}],
            "is_error": True
        }

@tool("api_post", "Send data to API", {"endpoint": str, "data": dict})
async def api_post(args):
    try:
        result = await api.post(args["endpoint"], args["data"])
        return {"content": [{"type": "text", "text": str(result)}]}
    except Exception as e:
        return {
            "content": [{"type": "text", "text": f"API Error: {e}"}],
            "is_error": True
        }

# Create server
api_server = create_sdk_mcp_server(
    name="api_client",
    version="1.0.0",
    tools=[api_get, api_post]
)

Tool Response Format

Tool handlers must return a dict with specific structure:

{
    "content": [
        {"type": "text", "text": "Result text"},
        # Optional: image content
        {"type": "image", "data": "base64...", "mimeType": "image/png"}
    ],
    "is_error": False  # Optional: True if error
}

Content Types:

  • Text: {"type": "text", "text": "..."}
  • Image: {"type": "image", "data": "base64data", "mimeType": "image/png"}

Error Responses:

Set "is_error": True to indicate failure:

return {
    "content": [{"type": "text", "text": "Error message"}],
    "is_error": True
}

Best Practices

  1. Descriptive Names: Use clear, action-oriented tool names (e.g., "search_database", not "tool1")

  2. Detailed Descriptions: Write descriptions that help Claude understand when to use the tool and what it does

  3. Type Safety: Use TypedDict or full JSON schemas for complex inputs

  4. Error Handling: Always handle exceptions and return error responses with is_error: True

  5. State Access: Tools can access application state directly (closure over variables)

  6. Async/Await: All tool handlers must be async functions

  7. Validation: Validate inputs in handler, even with schema validation

  8. Documentation: Document tool behavior in function docstrings

  9. Testing: Test tools independently before using with Claude

  10. Performance: In-process tools are fast; prefer them over subprocess MCP servers

Advantages of In-Process MCP Servers

  • Performance: No IPC overhead, direct function calls
  • Simplicity: Single process deployment
  • Debugging: Standard Python debugging works
  • State Access: Direct access to application variables
  • Type Safety: Full Python type hints and IDE support
  • Error Handling: Python exception handling
  • Testing: Standard Python testing frameworks

docs

agent-definitions.md

agents.md

client.md

configuration-options.md

content-blocks.md

core-query-interface.md

custom-tools.md

error-handling.md

errors.md

hook-system.md

hooks.md

index.md

mcp-config.md

mcp-server-configuration.md

messages-and-content.md

messages.md

options.md

permission-control.md

permissions.md

query.md

transport.md

COMPLETION_SUMMARY.md

tile.json