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.