Skip to content

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:

docs/examples/snippets/standalone_minimal.py
# [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:

docs/examples/snippets/standalone_resource.py
from litestar_mcp import MCP

mcp = MCP("my-mcp-server")


@mcp.resource(uri="app://system/status", name="system_status")
def get_status() -> "dict[str, str]":
    """Get the current system status."""
    return {"status": "healthy", "uptime": "up"}
docs/examples/snippets/standalone_prompt.py
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:

docs/examples/snippets/standalone_dependencies.py
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(...):

docs/examples/snippets/standalone_app.py
from litestar_mcp import MCP

mcp = MCP("my-mcp-server")

# [start-app]
# Access the underlying Litestar application
app = mcp.app
# [end-app]
docs/examples/snippets/standalone_custom.py
from litestar_mcp import MCP


class DummyPlugin:
    pass


plugin = DummyPlugin()

mcp = MCP(
    name="my-mcp-server",
    plugins=[plugin],  # Forwarded to Litestar
)

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:

docs/examples/snippets/standalone_run.py
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:

docs/examples/snippets/standalone_run_stdio.py
# [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