Standalone MCP Application (MCP)¶
The MCP class provides a declarative wrapper around Litestar and LitestarMCP for applications whose primary purpose is serving Model Context Protocol tools, resources, prompts, and Agent Skills.
1. Basic Setup and @mcp.tool()¶
Instantiate MCP with a server name and optional instructions, then decorate functions with @mcp.tool(). Function signatures and type annotations compile automatically into MCP tool JSON Schemas:
# [start-setup]
from litestar_mcp import MCP
mcp = MCP("my-mcp-server", instructions="Exposes utility tools.")
# [end-setup]
@mcp.tool(name="calculate_sum", description="Calculate the sum of two integers.")
def add(a: "int", b: "int") -> "int":
return a + b
2. Exposing Resources and Prompts¶
Use @mcp.resource() to register read-only data URIs (or RFC 6570 URI templates) and @mcp.prompt() to register reusable prompt templates:
from typing import Any
from litestar_mcp import MCP
mcp = MCP("my-mcp-server")
@mcp.prompt(name="explain_code", description="Ask the model to explain a code snippet.")
def explain(code: "str") -> "list[dict[str, Any]]":
return [
{
"role": "user",
"content": f"Explain this python code:\n\n{code}",
}
]
3. Dependency Injection and Route Options¶
Every @mcp.tool(), @mcp.resource(), and @mcp.prompt() decorator accepts standard Litestar route handler options (dependencies, guards, tags, summary, response_headers, opt, and sync_to_thread). Because name sets the MCP primitive identifier, pass route_name when you need a distinct Litestar route name:
from litestar.di import NamedDependency, Provide
from litestar_mcp import MCP
mcp = MCP("dependency-server")
def provide_account_id() -> "str":
return "acct-123"
@mcp.tool(
dependencies={"account_id": Provide(provide_account_id, sync_to_thread=False)},
sync_to_thread=False,
)
def current_account(account_id: "NamedDependency[str]") -> "dict[str, str]":
return {"account_id": account_id}
4. Accessing the Underlying Litestar App and Plugins¶
MCP.app lazily constructs the underlying Litestar instance once all decorators have been registered. You can also forward any Litestar constructor arguments (plugins, middleware, guards, stores, on_startup, on_shutdown) directly to MCP(...):
5. Running Over Streamable HTTP vs Stdio¶
Streamable HTTP (transport="streamable-http")¶
The default mcp.run() mode starts the Streamable HTTP endpoint (/mcp) through the Litestar CLI. Expose app = mcp.app at module top-level so the CLI and worker processes can import the ASGI application:
from litestar_mcp import MCP
mcp = MCP("my-mcp-server", instructions="Exposes utility tools.")
@mcp.tool()
def add(a: "int", b: "int") -> "int":
"""Calculate the sum of two integers."""
return a + b
# [start-run]
# Must expose the Litestar app instance globally so that the CLI can discover it
app = mcp.app
if __name__ == "__main__":
mcp.run(port=8000)
# [end-run]
Stdio Transport and MCPStdioContext (transport="stdio")¶
Set transport="stdio" to run in-process over standard input/output for desktop hosts (such as Claude Desktop or Cursor). Stdio enters Litestar.lifespan() so database pools, stores, and Dishka containers initialize in the exact same order as HTTP deployments.
Because stdio has no HTTP header layer, resolve caller identity from the host environment and pass it via MCPStdioContext:
# [start-run-stdio]
import os
from types import SimpleNamespace
from litestar import Request
from litestar_mcp import MCP, MCPStdioContext
mcp = MCP("stdio-server")
@mcp.tool()
def current_user(request: "Request") -> "dict[str, str]":
"""Return the current stdio identity."""
return {"user_id": request.user.id}
# Expose the app instance globally
app = mcp.app
if __name__ == "__main__":
user_id = os.environ.get("MCP_USER_ID", "stdio")
mcp.run(
transport="stdio",
stdio_context=MCPStdioContext(
user=SimpleNamespace(id=user_id),
auth={"sub": user_id},
),
)
# [end-run-stdio]
MCPStdioContext populates request.user, request.auth, request.session, and request.state on the synthetic ASGI scope for every stdio request:
MCPStdioContext Field |
Synthetic ASGI Scope Target | Purpose |
|---|---|---|
user |
scope["user"] (request.user) |
Authenticated user object inspected by route guards and tools |
auth |
scope["auth"] (request.auth) |
Authentication claims dictionary or token payload |
owner_id |
Task & session ownership key | Explicit owner identifier (falls back to user:<subject> derived from auth["sub"] or user.id) |
session |
scope["session"] (request.session) |
Litestar application session dictionary |
state |
scope["state"] (request.state) |
Per-request state dictionary merged into the synthetic scope |