# MCP prompts: slash commands your server ships

October 15, 2026 · Part 11 of 35

![MCP prompts: slash commands your server ships](https://pournasserian.com/_astro/lead.DCKOVvvP_Z1zIAWS.webp)

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](https://pournasserian.com/writing/mcp-2026-resources) 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](https://modelcontextprotocol.io/specification/2026-07-28/server/prompts) doesn't require any particular interface, but slash commands are the common pattern. Here is how hosts invoke a prompt, as of October 2026:

| Host | How a prompt is invoked |
|---|---|
| Claude (web, Desktop, mobile) | Chosen from the connector's menu in the chat |
| [Claude Code](https://code.claude.com/docs/en/mcp) | `/server:prompt (MCP)` in the `/` menu, or typed as `/mcp__server__prompt arg1 arg2` |
| [VS Code](https://code.visualstudio.com/docs/agent-customization/mcp-servers) (GitHub Copilot) | `/<server>.<prompt>` in the chat input |
| Gemini CLI | [Slash commands](https://cloud.google.com/blog/topics/developers-practitioners/gemini-cli-custom-slash-commands), with named (`--arg="value"`) or positional arguments |
| Cursor | The `/` prompt picker in the chat |
| ChatGPT | Not documented: [OpenAI's docs](https://developers.openai.com/plugins/build/mcp-server) don't describe MCP prompt support, and [a gateway vendor's docs](https://zuplo.com/docs/mcp-gateway/connect-clients/chatgpt.md) say ChatGPT doesn't currently consume arbitrary prompts from a remote server |

That sets a prompt apart from [server instructions](https://pournasserian.com/writing/mcp-2026-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:

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

`prompts/list` is paginated and cacheable, like the other lists ([part 6](https://pournasserian.com/writing/mcp-2026-caching-and-pagination) 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`:

```json
{
  "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:

```json
{
  "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):

```mermaid
sequenceDiagram
    actor U as User
    participant H as Host + client
    participant S as Server
    participant L as LLM

    H->>S: prompts/list
    S-->>H: prompts (name,<br/>title, arguments)<br/>+ ttlMs
    H->>U: show /code_review<br/>in the command<br/>menu
    U->>H: types /code_review<br/>and starts typing<br/>an argument
    H->>S: completion/complete<br/>(argument "repo",<br/>partial "web")
    S-->>H: suggestions<br/>["web-app",<br/>"web-api"]
    U->>H: picks values<br/>and submits
    H->>S: prompts/get<br/>(code_review,<br/>arguments)
    alt more input needed
        S-->>H: resultType<br/>input_required<br/>(e.g. which branch?)
        H->>U: elicitation form
        U-->>H: answer
        H->>S: prompts/get retry<br/>+ inputResponses<br/>+ requestState
    end
    S-->>H: messages (user /<br/>assistant roles,<br/>rich content)
    H->>L: inject messages<br/>into 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](https://pournasserian.com/writing/mcp-2026-what-changed): 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:

| Type | Notes |
|---|---|
| `text` | The common case |
| `image`, `audio` | Base64 data with a valid Multipurpose Internet Mail Extensions (MIME) type |
| `resource_link` | A Uniform Resource Identifier (URI) the client fetches, with resource annotations, as in [part 10](https://pournasserian.com/writing/mcp-2026-resources) |
| `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](https://py.sdk.modelcontextprotocol.io/servers/prompts/), `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.

```python
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:

```python
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:

```python
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)
```

```text
{
  "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](https://github.com/modelcontextprotocol/python-sdk/pull/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.

I'm [Amir Pournasserian](https://pournasserian.com/about). I build AI and platform systems for a living, maintain [FluentCMS](https://pournasserian.com/open-source/fluentcms) and [YeSvelte](https://pournasserian.com/open-source/yesvelte), and write here about what I find along the way.
