Standalone Application

The MCP class provides a high-level, developer-friendly entry point around Litestar and the LitestarMCP plugin, offering a declarative, simplified API.

It is the recommended entry point for projects where the primary purpose is exposing Model Context Protocol (MCP) tools, resources, and prompts, and you want to avoid boilerplates.

Basic Setup

To get started, instantiate the MCP class with a name and optional instructions.

from litestar_mcp import MCP

mcp = MCP("my-mcp-server", instructions="Exposes utility tools.")

Decorators

The application class provides dedicated decorators to expose Python functions as MCP primitives:

Exposing Tools

Use the @mcp.tool() decorator to register executable functions. The signature arguments are automatically analyzed and exposed as JSON Schema to the client.

@mcp.tool(name="calculate_sum", description="Calculate the sum of two integers.")
def add(a: "int", b: "int") -> "int":
    return a + b

Exposing Resources

Use the @mcp.resource() decorator to expose read-only data assets using URI templates (RFC 6570).

@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"}

Exposing Prompts

Use the @mcp.prompt() decorator to expose pre-defined templates or instruction sets for LLMs.

@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}",
        }
    ]

Route Handler Options

The standalone decorators accept the same route-handler keyword arguments as Litestar's @get / @post decorators, including dependencies, guards, response_headers, responses, summary, tags, DTO options, hooks, and arbitrary extra keyword arguments that Litestar stores in handler.opt. The name keyword is reserved for the MCP primitive name; use route_name when you need to set Litestar's route-handler name separately.

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(
    name="current_account",
    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}

Accessing the Litestar App

The MCP instance lazily instantiates the underlying Litestar application when the .app property is accessed. This ensures that all route handlers registered via decorators are captured.

# Access the underlying Litestar application
app = mcp.app

You can pass standard Litestar arguments (such as custom plugins, guards, or middleware) directly to the MCP constructor, and they will be forwarded to the Litestar instance.

from litestar_mcp import MCP


class DummyPlugin:
    pass


plugin = DummyPlugin()

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

Running the Server

The MCP class provides a .run() method to programmatically start the server.

By default, the server runs using the Server-Sent Events (SSE) transport by programmatically executing the standard Litestar CLI command.

# Must expose the Litestar app instance globally so that the CLI can discover it
app = mcp.app

if __name__ == "__main__":
    # Boot the server using the default Server-Sent Events (SSE) transport
    mcp.run(transport="sse", port=8000)

Exposing the Application

Because the SSE server is executed via the Litestar CLI (which starts uvicorn in subprocesses for worker scaling and reload features), the application instance must be importable from disk.

You must expose the underlying Litestar application instance globally (e.g. app = mcp.app) so that the CLI and worker processes can discover it. If the import path cannot be resolved, a RuntimeError will be raised.

Passing CLI Arguments

Any keyword arguments passed to mcp.run() when using SSE transport are mapped and forwarded directly to the corresponding Litestar CLI options (e.g., port becomes --port, reload becomes --reload).

Stdio Transport

To run the server over standard input/output (Stdio) for integration with local MCP clients (such as Claude Desktop), set the transport parameter to "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},
        ),
    )

When running over Stdio, the server manually drives the ASGI application's lifespan, ensuring that all dynamic startup and shutdown hooks registered by other plugins execute correctly.

Stdio does not have an HTTP header layer, so do not tunnel bearer headers through stdin. Resolve credentials from the host environment, operating-system profile, or another local mechanism, then pass the resulting identity with MCPStdioContext:

from types import SimpleNamespace

from litestar_mcp import MCPStdioContext

stdio_context = MCPStdioContext(
    user=SimpleNamespace(id="local-user"),
    auth={"sub": "local-user"},
    session={"tenant": "local"},
    state={"profile": "developer"},
)

mcp.run(transport="stdio", stdio_context=stdio_context)

Tools, resources, prompts, guards, and task execution receive these values through the synthetic Litestar request scope, so existing handler code can continue reading request.user, request.scope["auth"], request.scope["session"], and request.scope["state"]. Task ownership uses owner_id when set, then auth["sub"], then user.id / user.sub, and otherwise falls back to "stdio".