> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dedaluslabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

> Expose functions to AI agents with the @tool decorator

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

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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`:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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()`:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
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:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def test_add():
    assert add(2, 3) == 5
```

For tools using context, test the core logic separately (or use an integration-style harness).
