Back to blog
Engineering18 min read

MCP Prompt Templates and Slash Commands

MCP prompt templates are the protocol's least-used primitive: server-side prompts that clients surface as slash commands. What prompts/list returns, how to build one, and why teams should.

NH
Nafiul Hasan
Founder, Prompt Architects

TL;DR: MCP prompt templates are the protocol's third server primitive, alongside tools and resources. A server declares a prompts capability, answers prompts/list and prompts/get, and the client turns each template into something a person picks: in Claude Code, a slash command shaped like /mcp__servername__promptname. Tools are chosen by the model. Prompts are chosen by you.

Almost everything written about the Model Context Protocol is about tools. That is not an accident of taste. The protocol's own Build an MCP server tutorial lists prompts as one of three core concepts and then builds a server with tools only: as fetched on August 27, 2026, that page contains four registerTool calls and zero occurrences of prompts/list, registerPrompt or @mcp.prompt.

Which is a shame, because the prompts primitive solves a problem most teams actually have. Not "the model cannot reach my database" but "seven people are using seven slightly different versions of the same prompt, and nobody knows which one is current."

This page covers that primitive: what it is, what goes over the wire, how to build one, and where the client conventions diverge. For the Claude Code CLI surface instead, see using MCP inside Claude Code; for the no-terminal route, MCP for beginners.

What is the MCP prompts primitive?

It is a way for a server to publish reusable, parameterised prompt templates the user invokes by name. The specification's server overview puts the three primitives in a control hierarchy, and the column that matters is the one about who decides.

PrimitiveControlWhat the spec says it isSpec's example
PromptsUser-controlledInteractive templates invoked by user choiceSlash commands, menu options
ResourcesApplication-controlledContextual data attached and managed by the clientFile contents, git history
ToolsModel-controlledFunctions exposed to the LLM to take actionsAPI POST requests, file writing

That table is quoted from the specification's server overview at revision 2026-07-28, accessed August 27, 2026.

The prompts page is more explicit about what user-controlled means, and about a distinction people get wrong immediately: "This refers to who decides when the prompt is used, not who authors its content. Prompt content is defined by the server." You choose when. The server author chooses what.

The learn-level documentation describes prompts as "Pre-built instruction templates that tell the model to work with specific tools and resources", and elsewhere: "Prompts provide reusable templates. They allow MCP server authors to provide parameterized prompts for a domain, or showcase how to best use the MCP server."

Why does almost every MCP tutorial skip prompts?

Partly because tools are the demo that sells the protocol, and partly because the official material sets the pattern: the specification's own tools page runs to roughly two and a half times the length of its prompts page.

There is also a subtler reason. A tool is discoverable by the model, so a server author gets value from shipping one even if no human learns it exists. A prompt is only useful if a person goes looking for it, which makes prompts a documentation problem as much as an engineering one. Connect a server today, never type /, and you may never find out it ships prompts at all.

What do prompts/list and prompts/get return?

Two requests, both plain JSON-RPC 2.0. First the server has to advertise the capability at all:

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

At revision 2026-07-28 that block is declared in the server's DiscoverResult. listChanged says whether the server will announce changes to its prompt list later.

Discovery is prompts/list, which supports pagination and caching:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "prompts/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}

The response is a list of descriptors, not prompt text:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "prompts": [
      {
        "name": "code_review",
        "title": "Request Code Review",
        "description": "Asks the LLM to analyze code quality and suggest improvements",
        "arguments": [
          {
            "name": "code",
            "description": "The code to review",
            "required": true
          }
        ],
        "icons": [
          {
            "src": "https://example.com/review-icon.svg",
            "mimeType": "image/svg+xml",
            "sizes": ["any"]
          }
        ]
      }
    ],
    "nextCursor": "next-page-cursor",
    "ttlMs": 600000,
    "cacheScope": "public"
  }
}

Rendering is prompts/get, which takes the prompt name and an arguments object:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "prompts/get",
  "params": {
    "name": "code_review",
    "arguments": {
      "code": "def hello():\n    print('world')"
    }
  }
}

And the server hands back messages, ready to drop into the conversation:

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

Each message carries a role of either user or assistant and one content block. Content can be text, image, audio, a resource_link, or an embedded resource whose contents travel inline, which is how a server ships a style guide alongside the instruction without a second round trip.

How are prompt arguments declared?

Flatly, and this is the biggest structural difference from tools. A tool declares an inputSchema in JSON Schema and the model constructs a matching payload. A prompt declares a list of arguments, each with a name, an optional description and a required flag. That is the whole vocabulary in the specification's Prompt data type.

The Python SDK's prompts guide states it bluntly: "There is no JSON Schema here." Prompt arguments are, in its words, "a form a person fills in, not a payload a model constructs."

Here is what the SDK actually advertises from a decorated function:

{
  "name": "review_code",
  "description": "Review a piece of code.",
  "arguments": [
    {"name": "code", "required": true}
  ]
}

Add a title and per-argument descriptions and the client can draw a much better form:

{
  "name": "review_code",
  "title": "Code review",
  "description": "Review a piece of code.",
  "arguments": [
    {"name": "code", "description": "The code to review.", "required": true},
    {"name": "language", "description": "The language the code is written in.", "required": false}
  ]
}

How does a client turn a prompt into a slash command?

However it likes. The specification is deliberately non-prescriptive here: it shows a slash-command screenshot, says prompts "would be triggered through user-initiated commands in the user interface", and then adds that implementors are "free to expose prompts through any interface pattern that suits their needs".

Which means the invocation syntax is a client fact, not a protocol fact. Verified against each vendor's own documentation on August 27, 2026:

ClientHow a server prompt is invokedSource
Claude Code/mcp__servername__promptname, arguments space-separatedcode.claude.com/docs/en/mcp
VS Code/<MCP server>.<prompt> typed in the chat inputcode.visualstudio.com/docs
CursorPrompts listed as supported; no invocation syntax published on that pagecursor.com/docs/context/mcp

Anthropic's page states that "MCP prompts appear with the format" followed by that double-underscore form, and gives working examples:

/mcp__github__list_prs
/mcp__github__pr_review 456
/mcp__jira__create_issue "Bug in login flow" high

It also documents two behaviours worth designing around: "Prompt results are injected directly into the conversation", and "Server and prompt names are normalized, with spaces converted to underscores". So a prompt named review pr becomes review_pr in the command. Name your prompts as identifiers and the normalisation never surprises you.

VS Code's documentation describes the same feature in different words — "Use preconfigured prompt templates from MCP servers to standardize common tasks" — and a different syntax:

Type /<MCP server>.<prompt> in the chat input.

Cursor's MCP page lists Prompts under supported features, described as "Templated messages and workflows for users", but does not publish a command form on that page. Check your own client before you promise a team a specific keystroke.

How do you build a prompt server?

Less code than you expect. Here is a complete, runnable server in the TypeScript SDK, taken from the SDK's own prompts guide:

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const server = new McpServer({ name: 'review', version: '1.0.0' });

server.registerPrompt(
    'review-code',
    {
        title: 'Code Review',
        description: 'Review code for best practices and potential issues',
        argsSchema: z.object({
            code: z.string().describe('The code to review')
        })
    },
    ({ code }) => ({
        messages: [
            {
                role: 'user' as const,
                content: { type: 'text' as const, text: `Review this code:\n\n${code}` }
            }
        ]
    })
);

One Zod object does three jobs: it becomes the advertised argument list, validates prompts/get before your callback runs, and types the callback's parameters. The .describe() call survives into prompts/list as the argument description a client shows next to the field.

The Python equivalent is a decorator:

from mcp.server import MCPServer

mcp = MCPServer("Code Helper")


@mcp.prompt()
def review_code(code: str) -> str:
    """Review a piece of code."""
    return f"Please review this code:\n\n{code}"

Name from the function, description from the docstring, arguments from the parameters, required from the absence of a default. Add labels and descriptions when you want the form to explain itself:

from typing import Annotated

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Code Helper")


@mcp.prompt(title="Code review")
def review_code(
    code: Annotated[str, Field(description="The code to review.")],
    language: Annotated[str, Field(description="The language the code is written in.")] = "python",
) -> str:
    """Review a piece of code."""
    return f"Please review this {language} code:\n\n{code}"

A prompt does not have to be one message. Returning several lets you seed the shape of the answer, including the model's opening words:

server.registerPrompt(
    'explain-error',
    {
        description: 'Explain a compiler error and suggest the smallest fix',
        argsSchema: z.object({ error: z.string() })
    },
    ({ error }) => ({
        messages: [
            {
                role: 'user' as const,
                content: { type: 'text' as const, text: `Explain this compiler error:\n\n${error}` }
            },
            {
                role: 'assistant' as const,
                content: { type: 'text' as const, text: 'The one-line cause:' }
            }
        ]
    })
);

The SDK's note on that pattern is the useful bit: "The host hands the messages to the model in order, so the trailing assistant message becomes the start of its reply." That is prefill, standardised, shipped from a server, and available to everyone who connects.

You can also attach the team's own reference material rather than restating it in an f-string:

const styleGuide = '- Prefer const over let.\n- No single-letter identifiers.';

server.registerPrompt(
    'review-against-style',
    {
        description: 'Review code against the team style guide',
        argsSchema: z.object({ code: z.string() })
    },
    ({ code }) => ({
        messages: [
            {
                role: 'user' as const,
                content: {
                    type: 'resource' as const,
                    resource: { uri: 'doc://style-guide', mimeType: 'text/markdown', text: styleGuide }
                }
            },
            {
                role: 'user' as const,
                content: { type: 'text' as const, text: `Review this code against the style guide:\n\n${code}` }
            }
        ]
    })
);

To see it working before wiring it to a client, the Python SDK's guide points at the Inspector:

uv run mcp dev server.py

Open the Prompts tab, pick the prompt, and it renders the form from the argument list.

Can MCP prompt templates be dynamic?

Yes, in three distinct ways, and this is where the primitive stops being a static snippet library.

The list can change at runtime. A server that declared listChanged notifies connected clients:

{
  "jsonrpc": "2.0",
  "method": "notifications/prompts/list_changed"
}

Anthropic documents the receiving half: Claude Code supports these notifications, "allowing MCP servers to dynamically update their available tools, prompts, and resources without requiring you to disconnect and reconnect." In the Python SDK you register with mcp.add_prompt(...), remove with mcp.remove_prompt(name), and then announce it:

await ctx.notify_prompts_changed()
await ctx.session.send_prompt_list_changed()

The list can differ per caller. The 2026-07-28 specification says the advertised set must not "vary per-connection or as a side effect of other requests on the connection", but it may vary by the authorisation presented, "since credentials are per-request input, not connection state". So a paid tier, or an admin-only template, is a legitimate design rather than a hack.

A prompt can ask for more input before it resolves. The same revision allows a server to answer prompts/get with an InputRequiredResult, following the protocol's multi round-trip mechanism, and the client retries with inputResponses. A prompt that needs to know which environment you meant can ask.

What does argument autocompletion add?

A separate capability, declared separately:

{
  "capabilities": {
    "completions": {}
  }
}

When someone is filling in an argument, the client can ask the server what the valid values look like:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "completion/complete",
  "params": {
    "ref": {
      "type": "ref/prompt",
      "name": "code_review"
    },
    "argument": {
      "name": "language",
      "value": "py"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "completion": {
      "values": ["python", "pytorch", "pyside"],
      "total": 10,
      "hasMore": true
    }
  }
}

Completions cap at 100 values per response, and a client can pass earlier answers in a context.arguments object so later suggestions narrow accordingly: pick python for the language, and the framework field can suggest flask rather than everything.

In the TypeScript SDK you get all of that by wrapping one field:

import { completable } from '@modelcontextprotocol/server';

server.registerPrompt(
    'translate',
    {
        description: 'Translate a snippet into another language',
        argsSchema: z.object({
            language: completable(z.string(), value =>
                ['typescript', 'python', 'rust', 'go'].filter(language => language.startsWith(value))
            ),
            code: z.string()
        })
    },
    ({ language, code }) => ({
        messages: [{ role: 'user' as const, content: { type: 'text' as const, text: `Translate to ${language}:\n\n${code}` } }]
    })
);

The SDK's guide notes that the first completable field "also registers the server's completion/complete handler and advertises the completions capability", so there is nothing extra to declare.

Why is a server-side prompt template better than a shared doc?

Because a document is a copy instruction and a server is a distribution mechanism.

Think about how a good prompt currently spreads through a team. Someone writes it. It lands in Notion, a pinned Slack message, or a prompts/ folder nobody remembers. Everyone copies it. Then someone improves it, and two versions are in circulation with no way to tell which is which, because a pasted prompt carries no version number and no provenance.

A prompt template on an MCP server inverts every part of that:

  • One canonical copy. The template lives in the server's source, in your repository, reviewed like any other code.
  • Distribution is the connection. Anyone connected to the server has it. Nobody has to be told where the doc is.
  • Updates propagate. Redeploy, and the listChanged notification refreshes clients in place.
  • Arguments replace instructions. Instead of "remember to swap in your language", the client draws a required field.
  • Access follows credentials. The visible set can depend on the token, per the authorisation clause above.
  • It works the same in every client that supports the primitive, because the wire format is the protocol's, not yours.

For a team, the distribution step is usually project scope: commit the server to .mcp.json, and everyone who clones the repository gets the same templates. Post 235 covers that mechanism in detail. It is the same instinct behind version control for team prompts, except the enforcement happens at connection time rather than by asking people nicely.

The honest limitation: none of this helps the person who never types /. Prompts are user-controlled by design, which means discovery is on you. Ship a README line, or a tool whose description tells the model to suggest the matching prompt.

What actually goes wrong

Four things, in roughly the order teams hit them.

Error codes are not consistent across implementations. The specification says servers should return -32602 (Invalid params) for a missing required argument. The TypeScript SDK does exactly that, rejecting with a ProtocolError carrying -32602. The Python SDK's own prompts guide documents different behaviour for the same case: "the request itself fails with a JSON-RPC error (code -32603)". Both accessed August 27, 2026. Handle both codes on the client side rather than matching on one.

A failed prompt is not a failed tool call. The Python guide is explicit about why: "There is no tool-style error result to hand back to a model, because no model is in the loop: the call raises." Validate arguments in the client's form, not in the model's judgment.

Name normalisation changes your command. Spaces become underscores in Claude Code. If your prompt is called Weekly Report, the slash command is not what you wrote on the wiki.

Untrusted input reaches the model verbatim. The specification's security section on prompts requires implementations to "carefully validate all prompt inputs and outputs to prevent injection attacks or unauthorized access to resources". A prompt that interpolates a fetched web page into a message is a prompt injection vector, exactly as a tool would be.

The four prompt operations on our own server

Vendor section, so treat it as one. We run an MCP server at https://mcp.prompt-architects.com/mcp, authenticating with OAuth 2.1 or a pa_live_… personal access token. Connecting it in Claude Code is one command:

claude mcp add --transport http pa https://mcp.prompt-architects.com/mcp

It exposes four prompt operations, which our MCP integration page documents as slash commands in exactly the form Anthropic specifies:

/mcp__pa__improve
/mcp__pa__refine
/mcp__pa__shorten
/mcp__pa__enhance

improve rewrites for clarity, structure and specificity. refine asks one to three clarifying questions first, which makes it the one for a vague starting point. shorten compresses while preserving intent, for when you are up against a context window or a character limit. enhance adds role framing and depth, and is the heaviest of the four.

That figure is ours, from our own database, not third-party research. It is correlation and we know it: people who wire a server into their editor were the committed ones already. The gap is wide enough that we treat connecting MCP as the activation milestone that matters.

Two things we will not claim. Our own integration page is inconsistent about the identifier the model uses when calling these autonomously, so this post prints only the verified slash-command forms above. And our pricing page publishes no MCP row, so there is no MCP tier gate to describe: what is metered is enhancements, not the connection.

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 5.0★ on the Chrome Web Store.

Create An Account

Where to start

If you have never used one: connect a server you already trust, type / in Claude Code, and see whether anything with a double underscore appears. Several of the servers you have installed probably ship prompts you have never run.

If you are building one: take the smallest template your team already copy-pastes, register it with registerPrompt or @mcp.prompt(), and put it behind project scope so the whole repository gets it. Twenty lines is a realistic first version.

Wondering whether it is worth the trouble for a two-person team? Count how many places the same prompt currently lives. If the answer is more than one, the primitive is for you. Same reasoning as treating long-context templates as reusable assets rather than one-off messages.

Sources. MCP specification revision 2026-07-28 (server overview, prompts, completion) and the "Understanding MCP servers" and "Build an MCP server" guides at modelcontextprotocol.io; the TypeScript and Python SDK prompts guides in their repositories; code.claude.com/docs/en/mcp; the VS Code MCP servers page; Cursor's MCP page. All accessed August 27, 2026. The revision is dated in the URL, so check modelcontextprotocol.io/specification/latest before assuming a field still exists.

Frequently asked questions

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 5.0★ on the Chrome Web Store.

Create An Account