MCP prompts: slash commands your server ships

MCP prompts: slash commands your server ships

Of the three primitives a Model Context Protocol (MCP) server offers (tools, resources and prompts), prompts are the one the user picks. I like that split: your server writes them, hosts usually show them as slash commands or menu entries, and the user decides when to run one. So a prompt is a good way to package a workflow you know well, such as reviewing a pull request against your team’s standards or drafting an incident postmortem.

This is part 11 of my series on what an MCP server does under the 2026-07-28 specification. Part 10 covered resources, which the application controls. The facts are as I read them in October 2026.

In brief

  1. A prompt is a message template your server writes and the user invokes. “User-controlled” means the user decides when it’s used, not who writes it.
  2. prompts/list returns the templates; prompts/get renders one into messages in the user or assistant role, for the host to put into the conversation.
  3. completion/complete suggests argument values as the user types, and prompts/get may answer input_required to ask for a missing detail before it renders.
  4. Implementations MUST validate prompt inputs, which land in text the model treats as instructions. My advice goes further: keep free text between clear markers and design arguments as identifiers.
  5. In the Python software development kit (SDK), one decorator gives you the prompt and its arguments, but MCPServer answers a missing required argument or an unknown prompt with -32603 where the spec asks for -32602.

Who decides when a prompt runs

The prompts specification doesn’t require any particular interface, but slash commands are the common pattern. Here is how hosts invoke a prompt, as of October 2026:

HostHow a prompt is invoked
Claude (web, Desktop, mobile)Chosen from the connector’s menu in the chat
Claude Code/server:prompt (MCP) in the / menu, or typed as /mcp__server__prompt arg1 arg2
VS Code (GitHub Copilot)/<server>.<prompt> in the chat input
Gemini CLISlash commands, with named (--arg="value") or positional arguments
CursorThe / prompt picker in the chat
ChatGPTNot documented: OpenAI’s docs don’t describe MCP prompt support, and a gateway vendor’s docs say ChatGPT doesn’t currently consume arbitrary prompts from a remote server

That sets a prompt apart from server instructions: a host that supports them gives them to the model when it connects, and nobody picks them. A prompt arrives only when the user runs it.

How a prompt travels: list, complete, get

A server that offers prompts declares the capability, with listChanged if it will announce changes:

{ "capabilities": { "prompts": { "listChanged": true } } }

prompts/list is paginated and cacheable, like the other lists (part 6 covers both). Each prompt has a name and may have a title, a description, icons and arguments, each argument with a name, a description and required:

{
  "name": "code_review",
  "title": "Request code review",
  "description": "Review code quality and suggest improvements",
  "arguments": [{ "name": "code", "description": "The code to review", "required": true }]
}

prompts/get takes a name and arguments, and returns the rendered messages:

{
  "resultType": "complete",
  "description": "Code review prompt",
  "messages": [
    { "role": "user", "content": { "type": "text", "text": "Please review this Python code:\ndef hello():\n    print('world')" } }
  ]
}

Here is the whole path in a host with slash commands, from the listing to the large language model (LLM):

Host + client → Server: prompts/list; Server → Host + client: prompts (name, title, arguments) + ttlMs; Host + client → User: show /code_review in the command menu; User → Host + client: types /code_review and starts typing an argument; Host + client → Server: completion/complete (argument "repo", partial "web"); Server → Host + client: suggestions ["web-app", "web-api"]; User → Host + client: picks values and submits; Host + client → Server: prompts/get (code_review, arguments); alt: more input needed; Server → Host + client: resultType input_required (e.g. which branch?); Host + client → User: elicitation form; User → Host + client: answer; Host + client → Server: prompts/get retry + inputResponses + requestState; Server → Host + client: messages (user / assistant roles, rich content); Host + client → LLM: inject messages into the conversationLLMServerHost + clientalt[more input needed]Userprompts/listprompts (name,title, arguments)+ ttlMsshow /code_reviewin the commandmenutypes /code_reviewand starts typingan argumentcompletion/complete(argument "repo",partial "web")suggestions["web-app","web-api"]picks valuesand submitsprompts/get(code_review,arguments)resultTypeinput_required(e.g. which branch?)elicitation formanswerprompts/get retry+ inputResponses+ requestStatemessages (user /assistant roles,rich content)inject messagesinto the conversation

Three things deserve a word:

  • Completion. completion/complete lets a server suggest values for prompt arguments, and for resource-template arguments, as the user types. Suggested repository names, environments, customer IDs and date ranges make slash commands much easier to use. Declare the completions capability to support it.
  • Asking for input. prompts/get is one of the three methods allowed to return input_required, with tools/call and resources/read. A prompt can ask for a missing detail before it renders, through the Multi Round-Trip Requests (MRTR) flow from part 2: the alt block above.
  • Change notifications. Servers that declared listChanged SHOULD send notifications/prompts/list_changed to clients listening with promptsListChanged: true on subscriptions/listen. The client then calls prompts/list again.

What a message can hold

Prompt messages take the user or assistant role, so a prompt can carry few-shot examples or start an assistant turn. The content can be:

TypeNotes
textThe common case
image, audioBase64 data with a valid Multipurpose Internet Mail Extensions (MIME) type
resource_linkA Uniform Resource Identifier (URI) the client fetches, with resource annotations, as in part 10
resource (embedded)Server-managed content inlined into the message (URI, MIME type, text or blob)

All content types support annotations (audience, priority, lastModified).

Rules, and arguments the model can’t misread

The spec’s rules for prompts:

  • The prompt list MUST NOT vary per connection, but MAY vary by authorization, so you can show only the prompts the caller’s scopes allow.
  • Errors: -32602 for an unknown prompt or a missing required argument, -32603 for internal errors.
  • Servers SHOULD validate arguments, and clients SHOULD handle pagination.
  • Implementations MUST validate prompt inputs and outputs to prevent injection attacks or unauthorized access to resources.

The spec only asks you to validate. My advice goes further, because the injection risk is concrete: user-supplied arguments are interpolated into text the model treats as instructions.

  • Put arguments in clearly delimited blocks (“the code to review is between the markers below”), and cap their length. Never splice them into the instruction sentences themselves.
  • Design arguments as identifiers, such as a repository name or a ticket ID, not free text. Claude Code splits slash-command arguments on whitespace, so each argument must be a single token there.

A code review prompt in Python

With the official Python SDK, mcp 2.3.0, a prompt is a decorated function. This is the review prompt from my guide in Python, with the title from its listing and one addition: an optional repo argument for the diagram’s completion step.

from typing import Annotated

from pydantic import Field

from mcp import MCPError
from mcp.server import MCPServer
from mcp.types import INVALID_PARAMS

mcp = MCPServer("review-server")
REPOS = ["web-app", "web-api"]


@mcp.prompt(name="code_review", title="Request code review")
def code_review(
    code: Annotated[str, Field(description="The code to review")],
    repo: Annotated[str | None, Field(description="Repository name")] = None,
) -> str:
    """Review code against team standards"""
    if repo is not None and repo not in REPOS:
        raise MCPError(INVALID_PARAMS, f"Unknown repository: {repo}")
    header = f"Repository: {repo}\n" if repo else ""
    return f"""\
Review the code between the markers for correctness, security and style.
Report findings as a table with severity, line, issue and fix.
{header}<<<CODE
{code}
CODE>>>"""

The parameters become the prompt’s arguments, with their descriptions and required flags; the docstring becomes its description, and the returned string becomes one user message. repo is the shape that travels well: one token, checked against a list, with completion to help the user type it. code is free text, so it sits between the markers, and Claude Code would split it on whitespace. To stay short, the example leaves out a length cap.

Completion is one handler for the whole server; registering it makes the SDK declare the completions capability:

from mcp.types import Completion, PromptReference


@mcp.completion()
async def complete(ref, argument, context) -> Completion | None:
    if isinstance(ref, PromptReference) and argument.name == "repo":
        return Completion(values=[r for r in REPOS if r.startswith(argument.value)])
    return None

The in-memory client, in the same file, lists the prompt, completes “web”, gets it and tries three bad requests:

import anyio
from mcp import Client


async def main() -> None:
    async with Client(mcp) as client:  # 2026-07-28 by default
        listed = await client.list_prompts()
        print(listed.prompts[0].model_dump_json(by_alias=True, exclude_none=True, indent=2))

        ref = PromptReference(name="code_review")
        done = await client.complete(ref, {"name": "repo", "value": "web"})
        print(done.completion.values)

        code = "def hello():\n    print('world')"
        got = await client.get_prompt("code_review", {"code": code, "repo": "web-app"})
        print(got.messages[0].content.text)

        for name, args in [("code_review", {}), ("no_such_prompt", {"code": code}),
                           ("code_review", {"code": code, "repo": "other"})]:
            try:
                await client.get_prompt(name, args)
            except MCPError as e:
                print(e.error.code, e.error.message)


anyio.run(main)
{
  "name": "code_review",
  "title": "Request code review",
  "description": "Review code against team standards",
  "arguments": [
    {
      "name": "code",
      "description": "The code to review",
      "required": true
    },
    {
      "name": "repo",
      "description": "Repository name",
      "required": false
    }
  ]
}
['web-app', 'web-api']
Review the code between the markers for correctness, security and style.
Report findings as a table with severity, line, issue and fix.
Repository: web-app
<<<CODE
def hello():
    print('world')
CODE>>>
-32603 Internal server error
-32603 Internal server error
-32602 Unknown repository: other

The last three lines are the errors. For the first two the spec asks for -32602 (invalid params), but MCPServer in 2.3.0 answers both with -32603 (internal error). A fix for the unknown-prompt case, pull request #1872, was still open on 9 October 2026. For a missing argument, you can make the parameter optional and raise MCPError with INVALID_PARAMS yourself, as the repo check does. The client then gets -32602, but prompts/list shows the argument as required: false. An unknown prompt still gets -32603 in 2.3.0, because none of your code runs.

The full prompts/list result carried the SDK’s defaults, ttlMs: 0 and cacheScope: "private", and the prompts/get result carried resultType: "complete".

Method and caveats

  • Built from my guide to MCP servers and written against the 2026-07-28 specification.
  • How each host invokes prompts is as of October 2026.
  • The Python samples ran against mcp 2.3.0 on 9 October 2026, with the SDK’s in-memory client, and printed the output shown. The repo argument, its check and its completion handler are my additions to the sample in my guide.
  • Where OpenAI’s documentation and a gateway vendor’s are more precise than my notes (on prompts in ChatGPT), this article follows them and names the source.
Series What an MCP server actually does in 2026 Part 11 of 35
  1. What an MCP server does in 2026: much more than a list of tools
  2. MCP goes stateless: what changed in the 2026-07-28 specification
  3. server/discover: the one method every MCP server must implement
  4. Server instructions: the paragraph every model reads first
  5. Stateless MCP requests: _meta, resultType and explicit handles
  6. Caching hints and pagination in MCP
  7. MCP transports in 2026: stdio and Streamable HTTP
  8. Designing an MCP tool: the definition and the result
  9. MCP tool errors, and tools that models use well
  10. MCP resources: context the application decides to use
  11. MCP prompts: slash commands your server ships
  12. Tool, resource or prompt? Choosing the right MCP primitive

I’m Amir Pournasserian. I build AI and platform systems for a living, maintain FluentCMS and YeSvelte, and write here about what I find along the way.