Resources¶
Resources are read-only payloads such as schemas, capability summaries,
or cached projections. Tag a Litestar route handler with
mcp_resource="<resource_name>" and the plugin publishes it via
resources/list and resources/read. The task-manager demo
registers two:
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; user-supplied completion is planned but not yet
exposed through a stable API.
JSON-RPC Round-Trip¶
After initialize, clients drive resources via resources/list and
resources/read:
# List every resource marked in the application
curl -sS -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"resources/list","params":{}}'
# Read a resource by URI
curl -sS -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"resources/read",
"params":{"uri":"litestar://openapi"}}'
Successful responses carry the handler's return value inside the standard JSON-RPC envelope.
Error Contract¶
resources/read for an unknown URI returns MCP's spec-mandated
resource-not-found code -32002 with data.uri. When a marked
handler raises or returns an error during a read, the JSON-RPC code
is INTERNAL_ERROR (-32603) — the code reflects the
primitive-level error class, never the handler's HTTP status. The
original status is preserved in data.statusCode so clients can
recover the finer signal:
{"jsonrpc":"2.0","id":2,"error":{
"code":-32603,"message":"Resource read failed",
"data":{"statusCode":503,"content":{"error":"upstream timeout"}}}}
Note
The resource-not-found code -32002 is mandated by the current MCP
specification. An upstream proposal (SEP-2164) would migrate it to
-32602; this page will be updated if that lands.