Tools and Resources¶
Once marked, a route is ready to be called over MCP. This page walks through what registration looks like end-to-end, using the task-manager demo as a reference.
Tool Registration¶
Every mcp_tool handler becomes an entry in the plugin registry at
startup. 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.
Resource Registration¶
Resources follow the same pattern with the mcp_resource kwarg. The
same demo registers two read-only resources:
docs/examples/task_manager/main.py - register_resources¶def register_resources(store: "dict[int, Task]") -> "list[Any]":
"""Return read-only MCP resource handlers bound to ``store``."""
# start-example
@get("/tasks/schema", mcp_resource="task_schema")
async def get_task_schema() -> dict[str, Any]:
"""Get the task data model schema - exposed as MCP resource."""
return {
"type": "object",
"required": ["id", "title", "description"],
"properties": {
"id": {"type": "integer", "description": "Unique task identifier"},
"title": {"type": "string", "description": "Task title"},
"description": {"type": "string", "description": "Task description"},
"completed": {"type": "boolean", "description": "Task completion status", "default": False},
},
}
@get("/api/info", mcp_resource="api_info")
async def get_api_info() -> dict[str, Any]:
"""Get API information and capabilities - exposed as MCP resource."""
return {
"name": "Task Management API",
"version": "1.0.0",
"description": "Simple task management system with MCP integration",
"features": ["task_creation", "task_listing", "task_completion", "task_deletion"],
"endpoints_count": len(["/tasks", "/tasks/{task_id}", "/tasks/schema", "/api/info"]),
"mcp_integration": True,
}
# end-example
return [get_task_schema, get_api_info]
Marked resources appear in resources/list and are fetched via
resources/read. The plugin always ships one synthetic resource,
litestar://openapi, that returns the application's OpenAPI document.
Resource URI Templates¶
Pass mcp_resource_template="scheme://path/{var}" alongside
mcp_resource to register an RFC 6570 Level 1 URI template. Clients
can then request concrete URIs that match the template, and the plugin
passes the extracted variables straight through to the handler the same
way Litestar would bind path parameters on an HTTP request:
docs/examples/snippets/resource_template.py¶from litestar import Litestar, get
from litestar_mcp import LitestarMCP
@get(
"/workspaces/{workspace_id:str}/files/{file_id:str}",
mcp_resource="workspace_file",
mcp_resource_template="app://workspaces/{workspace_id}/files/{file_id}",
sync_to_thread=False,
)
def read_workspace_file(workspace_id: str, file_id: str) -> dict[str, str]:
"""Return the concrete workspace/file payload."""
return {"workspace": workspace_id, "file": file_id}
app = Litestar(route_handlers=[read_workspace_file], plugins=[LitestarMCP()])
Registered templates are announced via the resources/templates/list
JSON-RPC method, and concrete URIs flow through resources/read:
// Request
{"jsonrpc":"2.0","id":1,"method":"resources/read",
"params":{"uri":"app://workspaces/42/files/99"}}
// Response (extracted vars -> handler kwargs)
{"jsonrpc":"2.0","id":1,"result":{"contents":[
{"uri":"app://workspaces/42/files/99","mimeType":"application/json",
"text":"{\"workspace\":\"42\",\"file\":\"99\"}"}]}}
{var} matches a single non-empty path segment — it does NOT cross
/. Ambiguous templates resolve to the first-registered match. The
completion/complete JSON-RPC method is available but returns an empty
completion by default for 0.5.0; a @mcp_resource_completion decorator
is planned for a future release.
JSON-RPC Round-Trip¶
The MCP Streamable HTTP transport is a single JSON-RPC endpoint at
/mcp. Clients initialise once, then send tools/list, tools/call,
resources/list, and resources/read methods:
# 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":{}}}'
# Read a resource by URI
curl -sS -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"resources/read",
"params":{"uri":"litestar://openapi"}}'
Successful responses carry the handler's return value inside the standard JSON-RPC envelope. Errors raised from the underlying handler are mapped onto JSON-RPC error objects automatically.