Tools

Tools are executable operations — anything that takes arguments and returns structured output. Tag a Litestar route handler with mcp_tool="<tool_name>" and the plugin publishes it via tools/list and tools/call. The task-manager demo registers five tool handlers covering the full CRUD lifecycle:

docs/examples/task_manager/main.py - register_tools
def register_tools(store: "dict[int, Task]") -> "list[Any]":
    """Return MCP tool handlers (task CRUD) bound to ``store``."""

    # start-example
    @get("/tasks", mcp_tool="list_tasks")
    async def list_tasks(completed: "bool | None" = None) -> "list[Task]":
        """List all tasks, optionally filtered by completion status."""
        if completed is None:
            return list(store.values())
        return [task for task in store.values() if task.completed == completed]

    @get("/tasks/{task_id:int}", mcp_tool="get_task")
    async def get_task(task_id: int) -> Task:
        """Get a specific task by ID."""
        if task_id not in store:
            raise NotFoundException(detail=f"Task {task_id} not found")
        return store[task_id]

    @post("/tasks", status_code=HTTP_201_CREATED, mcp_tool="create_task")
    async def create_task(data: CreateTaskRequest) -> Task:
        """Create a new task."""
        new_id = max(store.keys(), default=0) + 1
        new_task = Task(id=new_id, title=data.title, description=data.description, completed=False)
        store[new_id] = new_task
        return new_task

    @post("/tasks/{task_id:int}/complete", mcp_tool="complete_task")
    async def complete_task(task_id: int) -> Task:
        """Mark a task as completed."""
        if task_id not in store:
            raise NotFoundException(detail=f"Task {task_id} not found")
        store[task_id].completed = True
        return store[task_id]

    @delete("/tasks/{task_id:int}", mcp_tool="delete_task")
    async def delete_task(task_id: int) -> None:
        """Delete a task by ID."""
        if task_id not in store:
            raise NotFoundException(detail=f"Task {task_id} not found")
        del store[task_id]

    # end-example
    return [list_tasks, get_task, create_task, complete_task, delete_task]

The handlers themselves are ordinary Litestar @get / @post / @delete callables — the only extra is the mcp_tool kwarg. Each tool is discoverable via tools/list and invocable via tools/call.

Tool arguments are validated against the handler's parsed_fn_signature before dispatch — the same model Litestar uses for ordinary HTTP request parsing. Missing required arguments surface as JSON-RPC INVALID_PARAMS (-32602). Annotated[T, Parameter(...)] query arguments are unwrapped and their Parameter constraints (ge / le / min_length / pattern / …) flow through into the advertised inputSchema.

JSON-RPC Round-Trip

After initialize, clients drive tools via tools/list and tools/call:

# Initialise the server
curl -sS -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-11-25","capabilities":{},
       "clientInfo":{"name":"curl","version":"1.0"}}}'

# List every tool marked in the application
curl -sS -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

# Execute a specific tool (task-manager demo)
curl -sS -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
       "params":{"name":"list_tasks","arguments":{}}}'

Successful responses carry the handler's return value inside the standard JSON-RPC envelope.

Error Contract

Tool errors are reported differently from the other primitives. A handler that raises or returns an error response is surfaced inside the tool result with isError: true (and the detail in content), not as a JSON-RPC error object — this lets the model see and react to the failure. Only protocol-level problems (an unknown tool name, malformed params) use a JSON-RPC error with INVALID_PARAMS (-32602). See Prompts and Resources for how those primitives map the handler's HTTP status onto JSON-RPC codes.