Skip to main content
Tools let agents call your Python functions. Decorate, register, serve. Tools are the core building blocks that allow an MCP client to invoke your Python functions via the MCP protocol:
  1. A client discovers tools via tools/list (each tool includes inputSchema and optional outputSchema).
  2. A client calls a tool via tools/call with arguments matching the schema.
  3. The server executes your callable.
  4. The server returns a CallToolResult containing content (and optionally structuredContent) per MCP Spec.

Basic tool

from dedalus_mcp import MCPServer, tool

@tool(description="Add two numbers")
def add(a: int, b: int) -> int:
    return a + b

server = MCPServer("math")
server.collect(add)
The description tells the LLM what the tool does. Type hints become JSON Schema.

Async tools

import anyio
from dedalus_mcp import tool

@tool(description="Fetch user data (simulated I/O)")
async def get_user(user_id: str) -> dict:
    await anyio.sleep(0.1)
    return {"user_id": user_id, "status": "ok"}
Prefer async for I/O. Important: in Dedalus MCP, sync tools run inline (they are not automatically moved to a thread pool). If you need concurrency for blocking work, use async def and offload explicitly.

Type inference

Type hints become JSON Schema automatically:
from typing import Literal
from pydantic import BaseModel
from dedalus_mcp import tool

class SearchFilters(BaseModel):
    category: str | None = None
    min_price: float = 0.0

@tool(description="Search products")
def search(
    query: str,
    limit: int = 10,
    sort: Literal["relevance", "price", "date"] = "relevance",
    filters: SearchFilters | None = None,
) -> list[dict]:
    return [{"query": query, "limit": limit, "sort": sort, "filters": filters.model_dump() if filters else None}]
Supported: primitives, list, dict, Literal, Enum, optionals/unions, Pydantic models, dataclasses, nested models. Required parameters have no default. Optional parameters have one.

Decorator options

from dedalus_mcp import tool

@tool(
    name="find_products",           # Override tool name
    description="Search catalog",   # Tool description
    tags={"search", "catalog"},     # For filtering/metadata
)
def search_products_impl(query: str) -> list[dict]:
    return [{"id": "p_1", "name": "Widget", "query": query}]

Structured returns

Return JSON-serializable values:
from dedalus_mcp import tool

@tool(description="Analyze text")
def analyze(text: str) -> dict:
    return {"word_count": len(text.split()), "char_count": len(text)}
For explicit control, return CallToolResult:
from dedalus_mcp import tool
from dedalus_mcp.types import CallToolResult, TextContent

@tool(description="Custom result")
def custom() -> CallToolResult:
    return CallToolResult(
        content=[TextContent(type="text", text="Custom message")],
        isError=False,
    )

Context access

Logging and progress via get_context():
import anyio
from dedalus_mcp import tool, get_context

@tool(description="Process files with progress reporting")
async def process_files(paths: list[str]) -> dict:
    ctx = get_context()
    await ctx.info("Starting", data={"count": len(paths)})

    processed = 0
    try:
        async with ctx.progress(total=len(paths)) as tracker:
            for path in paths:
                # Simulate work; cancellation is delivered as task cancellation
                await anyio.sleep(0.01)
                processed += 1
                await tracker.advance(1)
    except anyio.get_cancelled_exc_class():
        await ctx.warning("Cancelled", data={"processed": processed})
        raise

    return {"processed": processed}

Allow-lists

Restrict visible tools:
from dedalus_mcp import MCPServer, tool

@tool(description="Add")
def add(a: int, b: int) -> int:
    return a + b

@tool(description="Multiply")
def multiply(a: int, b: int) -> int:
    return a * b

server = MCPServer("gated")
server.collect(add, multiply)
server.allow_tools({"add"})
Calling a hidden tool returns an error CallToolResult indicating the tool is not available.

Error handling

Raise exceptions normally:
from dedalus_mcp import tool

@tool(description="Divide")
def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

Testing

Test tools as normal functions:
def test_add():
    assert add(2, 3) == 5
For tools using context, test the core logic separately (or use an integration-style harness).