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:
- A client discovers tools via
tools/list (each tool includes inputSchema and optional outputSchema).
- A client calls a tool via
tools/call with arguments matching the schema.
- The server executes your callable.
- The server returns a
CallToolResult containing content (and optionally structuredContent) per MCP Spec.
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.
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). Last modified on January 27, 2026