Marking Routes & Decorators¶
litestar-mcp discovers tools, resources, and prompts by inspecting Litestar route handlers at application startup (on_app_init). You can mark handlers using either route keyword arguments (handler.opt) or dedicated decorators (@mcp_tool, @mcp_resource, @mcp_prompt).
Route Keyword Arguments vs Decorators¶
Litestar automatically forwards unknown keyword arguments on route decorators (@get, @post, @put, @patch, @delete, @route) into handler.opt. Passing mcp_tool=True (or a custom string name like mcp_tool="get_user") directly to the route decorator is the most concise way to expose an existing endpoint.
The @mcp_tool, @mcp_resource, and @mcp_prompt decorators store metadata in MetadataRegistry and are useful when you want IDE autocompletion on MCP-specific options (input_schema, output_schema, annotations, scopes, task, queue) or when declaring standalone prompt functions.
"""Snippet: marking a route as an MCP tool. Referenced from docs/usage/marking.rst."""
from litestar import Litestar, get
from litestar.params import FromPath
from litestar_mcp import LitestarMCP
def build() -> "Litestar":
# start-example
@get("/users/{user_id:int}", mcp_tool=True)
async def get_user(user_id: "FromPath[int]") -> "dict[str, int]":
"""Return a user by ID - exposed as the ``get_user`` MCP tool."""
return {"id": user_id}
app = Litestar(route_handlers=[get_user], plugins=[LitestarMCP()])
# end-example
return app
"""Snippet: using the mcp_tool/mcp_resource decorators. Referenced from docs/usage/marking_routes.rst."""
from litestar import Litestar, get
from litestar.params import FromPath
from litestar_mcp import LitestarMCP
from litestar_mcp.utils import mcp_tool
def build() -> "Litestar":
# start-example
@mcp_tool()
@get("/users/{user_id:int}")
async def get_user(user_id: "FromPath[int]") -> "dict[str, int]":
"""Return a user by ID - exposed via the ``mcp_tool`` decorator."""
return {"id": user_id}
app = Litestar(route_handlers=[get_user], plugins=[LitestarMCP()])
# end-example
return app
Choosing a Convention
Both forms compile into the exact same Registry entry on route handlers. When both a decorator and a handler.opt key define the same field on a route, handler.opt takes precedence. Note that @mcp_prompt on a plain function registers a standalone MCP-only prompt via LitestarMCP(prompts=[...]), whereas mcp_prompt=True on a Litestar route handler exposes the endpoint over both HTTP and prompts/get.
Marking Tools, Resources, and Prompts¶
Tool Markers¶
Pass mcp_tool=True to publish a route handler under its Python function name, or mcp_tool="<custom_name>" to override the advertised tool name:
"""Snippet: marking a route as an MCP tool. Referenced from docs/usage/marking.rst."""
from litestar import Litestar, get
from litestar.params import FromPath
from litestar_mcp import LitestarMCP
def build() -> "Litestar":
# start-example
@get("/users/{user_id:int}", mcp_tool=True)
async def get_user(user_id: "FromPath[int]") -> "dict[str, int]":
"""Return a user by ID - exposed as the ``get_user`` MCP tool."""
return {"id": user_id}
app = Litestar(route_handlers=[get_user], plugins=[LitestarMCP()])
# end-example
return app
Resource Markers¶
Pass mcp_resource=True or mcp_resource="<resource_name>" to publish a read-only handler in resources/list and resources/read:
"""Snippet: marking a route as an MCP resource. Referenced from docs/usage/marking.rst."""
from typing import Any
from litestar import Litestar, get
from litestar_mcp import LitestarMCP
def build() -> "Litestar":
# start-example
@get("/api/schema", mcp_resource="api_schema")
async def get_schema() -> "dict[str, Any]":
"""Return the API JSON schema - exposed as the ``api_schema`` MCP resource."""
return {"type": "object", "properties": {}}
app = Litestar(route_handlers=[get_schema], plugins=[LitestarMCP()])
# end-example
return app
Prompt Markers¶
Prompts can be registered either as route-bound handlers (mcp_prompt=True or mcp_prompt="<prompt_name>") or as standalone @mcp_prompt callables passed to LitestarMCP(prompts=[...]):
"""Snippet: marking and registering MCP prompts. Referenced from docs/usage/marking_routes.rst."""
from litestar import Litestar, get
from litestar.params import FromQuery
from litestar_mcp import LitestarMCP, mcp_prompt
def build() -> "Litestar":
# start-example
@mcp_prompt(description="Summarise a document in a chosen style.")
async def summarize(text: "str", style: "str" = "concise") -> "str":
"""Build a summarisation prompt.
Args:
text: The document to summarise.
style: Summary style, e.g. ``concise`` or ``detailed``.
"""
return f"Summarise the following in a {style} style:\n\n{text}"
@get(
"/prompts/code-review",
mcp_prompt=True,
mcp_prompt_description="Ask the model to review a code diff.",
)
async def code_review(code: "FromQuery[str]") -> "dict[str, object]":
"""Handler-based prompt: routed under HTTP *and* exposed via ``prompts/get``."""
return {"messages": [{"role": "user", "content": {"type": "text", "text": f"Review: {code}"}}]}
app = Litestar(
route_handlers=[code_review],
plugins=[LitestarMCP(prompts=[summarize])],
)
# end-example
return app
MCPOptKeys Reference Table¶
Downstream applications can customize every handler.opt key name via MCPConfig(opt_keys=MCPOptKeys(...)) to avoid collisions with other plugins. All keys default to mcp_<purpose>:
MCPOptKeys Attribute |
Default handler.opt Key |
Applies To | Description |
|---|---|---|---|
tool |
"mcp_tool" |
Tools | Marks a route handler as an MCP tool (True uses the function name; a str sets an explicit tool name). |
resource |
"mcp_resource" |
Resources | Marks a route handler as an MCP resource (True uses the function name; a str sets an explicit resource name). |
resource_uri |
"mcp_resource_uri" |
Resources | Overrides the default litestar://<name> resource URI with a concrete URI. |
resource_mime_type |
"mcp_resource_mime_type" |
Resources | Overrides the resource MIME type advertised in resources/list and used as a fallback in resources/read. |
resource_template |
"mcp_resource_template" |
Resources | Declares an RFC 6570 Level 1 URI template (for example, "app://workspaces/{workspace_id}/files/{file_id}"). |
prompt |
"mcp_prompt" |
Prompts | Marks a route handler as an MCP prompt (True uses the function name; a str sets an explicit prompt name). |
description |
"mcp_description" |
Tools | Overrides the primary LLM-facing tool description. |
resource_description |
"mcp_resource_description" |
Resources | Overrides the primary resource description (distinct from mcp_description so dual-role handlers can carry separate prose). |
prompt_description |
"mcp_prompt_description" |
Prompts | Overrides the primary prompt description. |
prompt_title |
"mcp_prompt_title" |
Prompts | Sets an optional human-readable display title for UI clients. |
prompt_arguments |
"mcp_prompt_arguments" |
Prompts | Explicit list[dict[str, Any]] of prompt argument definitions (name, title, description, required). |
prompt_icons |
"mcp_prompt_icons" |
Prompts | Optional list[dict[str, Any]] of MCP icon descriptors (src, mimeType, sizes). |
when_to_use |
"mcp_when_to_use" |
Tools, Resources | Appends a structured ## When to use section to the rendered description. |
returns |
"mcp_returns" |
Tools, Resources | Appends a structured ## Returns section to the rendered description. |
agent_instructions |
"mcp_agent_instructions" |
Tools, Resources | Appends a mandatory ## Instructions grounding section to the rendered description. |
annotations |
"mcp_annotations" |
Tools | Mapping of MCP ToolAnnotations hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint, scopes). |
read_only |
"mcp_read_only" |
Tools | Boolean shorthand setting annotations["readOnlyHint"]. |
required_client_capabilities |
"mcp_required_client_capabilities" |
Tools | Mapping of client capabilities required before tools/call is allowed (returns -32021 when missing). |
task / task_support |
"mcp_task" / "mcp_task_support" |
Tools | Declares MCP Tasks support mode ("optional", "required", "forbidden", or aliases True, False, "sync", "disabled", "inline"). Can be set on a route, Controller, or Router. |
task_input_before_start |
"mcp_task_input_before_start" |
Tools | Executes the initial tools/call synchronously so an MCPInputRequiredResult round completes before creating a background task. |
queue |
"mcp_queue" |
Tools | Overrides the target litestar-queues queue lane name for task-augmented tool execution, or False / "in_process" to force in-process execution. |
Description Precedence & Docstring Section Stripping¶
When rendering the LLM-facing description for a tool, resource, or prompt (extract_description_sources and render_description), litestar-mcp resolves prose in the following precedence order:
- Kind-Specific
handler.optOverride:handler.opt["mcp_description"](tools),handler.opt["mcp_resource_description"](resources), orhandler.opt["mcp_prompt_description"](prompts). - Decorator Metadata:
description=passed to@mcp_tool,@mcp_resource,@mcp_prompt,@tool, or@prompt. - Litestar Route OpenAPI Attributes:
handler.description, falling back tohandler.summary(set via@get(..., description="...", summary="...")). - Stripped Function Docstring (
strip_docstring_sections): The function's__doc__with all Google-style (Args:,Returns:,Raises:,Notes:,Examples:), NumPy-style (Parameters\n---), and Sphinx (:param:,:returns:) sections automatically stripped. - Deterministic Fallback:
"Tool: <name>","Resource: <name>", or"Prompt: <name>".
Why Docstring Sections Are Stripped¶
Litestar route handlers frequently document dependency-injected parameters (such as db_session, current_user, or settings) in their Args: docstring block alongside client-supplied parameters. strip_docstring_sections removes the Args: / Parameters: / Returns: blocks from the top-level tool description so internal dependency names never leak into the LLM's tool summary, while per-parameter descriptions for advertised client arguments are still extracted and attached to their corresponding inputSchema.properties.<param>.description entries.
If when_to_use, returns, or agent_instructions are configured via handler.opt or @mcp_tool / @mcp_resource, render_description appends them as structured Markdown sections (## When to use, ## Returns, ## Instructions). CLI commands (litestar mcp list-tools, litestar mcp list-resources) pass structured=False to keep terminal output concise.
Universal Dependency Injection¶
Because litestar-mcp dispatches tools, resources, and route-bound prompts through an internal ASGI scope (execute_tool and execute_handler), every dependency injection mechanism supported by your application works identically over Streamable HTTP, in-process stdio, and background QueueTaskExecutor workers:
- Litestar
Provide&Dependency: Route-, controller-, router-, and app-leveldependencies={"key": Provide(...)}andNamedDependency[T]parameters are resolved automatically and hidden from the advertised MCPinputSchema(unless a provider itself takes client-facing input parameters). - Dishka (
FromDishka[T]): Handlers usingFromDishka[T]are unwrapped via__dishka_orig_func__during schema generation soFromDishkaparameters are excluded frominputSchemaand resolved from the activeScope.REQUESTDishka container at execution time. - Standalone
@toolDI (ToolContext,Inject(),Depends(fn)): Standalone@toolfunctions andSkillControllermethods can declareToolContext,Inject("key")(resolving fromapp.dependenciesorapp.state),Depends(fn), orFromDishka[T]parameters alongside client arguments.
"""Snippet: request-scoped dependency injection. Referenced from docs/usage/dependencies.rst."""
from typing import Any
from litestar import Litestar, get
from litestar.di import NamedDependency, Provide
from litestar_mcp import LitestarMCP
async def provide_current_user() -> "dict[str, str]":
return {"id": "u-1", "role": "admin"}
def build() -> "Litestar":
# start-example
@get("/me", mcp_tool=True, dependencies={"current_user": Provide(provide_current_user)})
async def whoami(current_user: "NamedDependency[dict[str, Any]]") -> "dict[str, Any]":
"""Return the resolved current user - exposed as the ``whoami`` MCP tool."""
return current_user
app = Litestar(route_handlers=[whoami], plugins=[LitestarMCP()])
# end-example
return app