Skip to content

Prompts & PromptController

Prompts are reusable, parameterized instruction templates exposed via prompts/list and prompts/get. Unlike tools (which execute side effects or computations) and resources (which return raw state), a prompt returns a structured sequence of PromptMessage objects that the client feeds into an LLM conversation.

litestar-mcp supports three ways to author prompts:

  1. Standalone @mcp_prompt functions registered via LitestarMCP(prompts=[...]).
  2. Route-bound mcp_prompt=True handlers exposed over both HTTP and prompts/get.
  3. Class-based PromptController subclasses grouping related @prompt methods under a shared namespace prefix, prepended instructions, Litestar guards, and dependency injection (Provide, FromDishka[T]).

Standalone & Route-Bound Prompts

Use @mcp_prompt for MCP-only prompt templates, or tag an existing Litestar route handler with mcp_prompt=True (or mcp_prompt="<name>") when the prompt should also be callable as a standard HTTP endpoint:

docs/examples/snippets/marking_prompts.py
"""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

For route-bound prompts, you can customize metadata with mcp_prompt_description, mcp_prompt_title, mcp_prompt_arguments, and mcp_prompt_icons (see the MCPOptKeys Reference Table). When arguments is omitted, litestar-mcp introspects the callable's signature, excludes DI parameters, and populates argument descriptions from the function's Google-style Args: docstring section.

Class-Based PromptController

When building domain-specific prompt suites or multi-agent applications, subclass PromptController and decorate methods with @prompt:

docs/examples/agent/coffee_shop_agent.py
class CoffeePromptController(PromptController):
    """Prompt templates for greeting customers and authoring seasonal menus."""

    prefix = "coffee"
    instructions = (
        "Persona: Warm, knowledgeable neighborhood specialty coffee barista.\n"
        "Tone: Welcoming, concise, and focused on quality craft brewing.\n"
    )

    @prompt(title="Welcome Greeting", description="Greet a customer and introduce daily specials.")
    def welcome_customer(self, customer_name: str = "Friend") -> str:
        """Generate a warm barista greeting."""
        return f"Welcome to the coffee bar, {customer_name}! What can we craft for you today?"

    @prompt(title="Tasting Notes", description="Generate tasting notes for a given roast.")
    def roast_notes(self, roast_name: str) -> list[PromptMessage]:
        """Produce structured tasting notes for single-origin coffees."""
        return [
            PromptMessage.user(
                f"Produce professional SCA-style tasting notes and brewing parameters for `{roast_name}`."
            )
        ]

Register a PromptController either by adding it to Litestar(route_handlers=[CoffeePromptController]) or by passing it in MCPConfig(prompt_controllers=[CoffeePromptController]).

PromptController Capabilities

  • Namespace prefix: Setting prefix = "coffee" registers @prompt methods as coffee/welcome_customer and coffee/roast_notes (qualify_prompt_name).
  • Prepended instructions (apply_prompt_instructions): When instructions is defined on a PromptController, prompts/get automatically prepends f"{instructions.strip()}\n\n" to the text of the first "user" role message (or inserts a leading "user" text message if no user text message is present).
  • Internal Dispatch Protection: Each @prompt method is mounted on an internal synthetic controller path (/_litestar_mcp/<module>.<qualname>) with include_in_schema=False and protected by the require_internal_dispatch guard so it can only be invoked through MCP internal ASGI dispatch (execute_handler), never as a public unguarded HTTP POST route.
  • Controller guards and Universal DI: Because PromptController subclasses litestar.Controller, you can declare guards = [...] and dependencies = {"key": Provide(...)} at the class level, and inject Provide dependencies or Dishka FromDishka[T] parameters directly into @prompt methods.

Return Value Normalization

Every prompt callable or @prompt method can return any of the following Python types, which _normalize_prompt_result converts into the canonical MCP messages list:

Return Type Normalized Wire Output (result.messages)
str [{"role": "user", "content": {"type": "text", "text": "<value>"}}]
PromptMessage [message.to_dict()] (use PromptMessage.user("...") or PromptMessage.assistant("..."))
dict[str, Any] with "messages" Used directly as the top-level prompts/get result dictionary (with controller instructions applied to result["messages"]).
dict[str, Any] with "role" & "content" Wrapped in a single-element list [result].
Sequence[PromptMessage] or list[dict] Each element is normalized in order into a PromptMessage dictionary.
Any other object Coerced via str(value) into a single "user" text message.

Capability Gating & Error Contract

  • Capability Gating: server/discover and initialize advertise "prompts": {"listChanged": True} under capabilities only when at least one visible prompt is registered.
  • String-Only Wire Arguments: Per the MCP specification, all values inside params.arguments on prompts/get must be strings. Passing a non-string argument value, omitting a required argument, or passing an unknown argument name raises JSON-RPC INVALID_PARAMS (-32602).
  • Execution Errors: If a prompt callable or route handler raises an exception or returns an HTTP error status during prompts/get, MCPHandlerService raises JSON-RPC INTERNAL_ERROR (-32603) with "message": "Prompt execution failed" and preserves the HTTP status code in error.data.statusCode.