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.
Binary Resources¶
Resources can return text or binary content. JSON and text-compatible
responses are returned as MCP text resource contents. Other byte
responses are base64 encoded as MCP blob contents:
docs/examples/snippets/resource_binary.py - route kwarg form¶@get(
"/reports/latest",
mcp_resource="latest_report",
mcp_resource_mime_type="application/pdf",
)
async def latest_report() -> "Response[bytes]":
return Response(content=b"...pdf bytes...", media_type="application/pdf")
mcp_resource_mime_type is advertised in resources/list and is used as
a fallback when the handler response does not carry a content type. The
decorator form accepts the same metadata as mime_type=:
docs/examples/snippets/resource_binary.py - decorator form¶@mcp_resource("archived_report", mime_type="application/pdf")
@get("/reports/archive")
async def archived_report() -> "Response[bytes]":
return Response(content=b"...pdf bytes...", media_type="application/pdf")
For tools that produce files, prefer returning
MCPResourceLink when the bytes are available through a
resource URI. Return MCPBlobResource from the tool only
when the bytes must be embedded immediately.
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¶
Clients drive resources with independent resources/list and
resources/read requests; see Migrating to MCP 0.12.0 for the mandatory
metadata and headers.
# 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 INVALID_PARAMS (-32602)
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.