Skip to content
Chapter 1 of 6 · PromptsUser-controlled prompts

14 min read

User-controlled prompts

On this page

A server speaking the Model Context Protocol — MCP, the open protocol that lets a chat application talk to outside capabilities over a standard wire format, so that one incidents service can be plugged into whatever assistant a team already uses — offers three kinds of thing across that wire. Tools are functions the model can call. Resources are documents the application can attach. The third kind is the subject of this topic: a prompt is a named template for a piece of conversation, written and stored by the server, which a person picks from a menu in the host application. The client then asks the server for it, the server answers with ready-made conversation turns, and the host drops those turns into the live chat as though the user had typed them.

Prompts exist because a good deal of what a server's authors know takes the form of a well-worded request rather than of a callable function or a stored document — which sections a postmortem needs, which facts to establish before anyone guesses at a cause, which question to ask the person before starting. Until there was somewhere to put that, it lived in a wiki page people copy-pasted from at 03:00, and it decayed the way wiki pages do. Packaging it as a prompt means the server ships the wording next to the capability, updates it in one place, and can fill parts of it in from live data at the moment somebody asks for it.

In practice you meet prompts as menu entries contributed by a connected server: the slash commands that appear in a coding assistant when a server attaches, the items behind the plus or paperclip menu of a desktop chat client, grouped under the server's name, or a command-palette entry in an IDE. Selecting one, filling in a short form of arguments, and watching a paragraph or two land in the composer — the box the user types into — is the whole of the user-facing experience. Where a capability has been registered as a tool instead, none of that surface exists: the user never sees a menu entry, because there is nothing on the menu to see, and the workflow reaches them only through whatever the model decides to do.

What the model was allowed to decide

Consider one capability, triage_incident, that gathers recent deploys, summarises a failure signature, and opens a postmortem document. It can be registered either way. Both versions ship the same text, the same argument, and largely the same server code. The property that differs is who is permitted to start it.

A tool is model-controlled: its description is written for the model, the model calls it mid-turn when the conversation seems to warrant it, and no human decision precedes that call. A prompt is user-controlled: it exists as an entry in the host's interface, a person selects it, and only then does the client call the server. The messages that come back are placed into the live conversation as turns the user is taken to have made.

This is the same distinction as a deploy target in a Makefile and a deploy step in a webhook handler. Both run the same script, both may produce identical output, and in the review afterwards only one question matters: could this have fired while nobody was looking? A Makefile target requires a person at a terminal. A webhook handler requires a matching event, and the definition of a matching event lives in code somebody wrote months ago against conditions they were guessing at. A model's judgement about whether a pasted stack trace warrants triage is the webhook's matching rule, except it is re-derived on every turn from whatever text happens to be in context.

That comparison is an analogy and it breaks in a place worth naming, because the place it breaks is the whole subject of the later chapters. A Makefile target and a webhook handler run the same script and produce the same output; the only variable is what caused them to run, and the analogy is exact on that variable alone. A prompt does not run a script. It returns conversation into a context that also contains tools, so what actually happens afterwards depends on a model reading that conversation. The analogy is therefore reliable for the question "could this have fired while nobody was looking" and unreliable for "what will it have done by the time somebody looks", and treating it as covering both is how a workflow gets shipped on the strength of a single sentence about initiation.

The two primitives are declared separately during the initialize exchange, so a host knows from the capability object which of its two surfaces each server offering belongs on before it makes a single call. A server that declares prompts and a server that declares tools are asking the host for different things: menu space in one case, and a slot in the model's callable inventory in the other.

The symptoms follow directly. Under the tool design, you observe workflows starting on the model's reading of ambiguous context — a postmortem opened because somebody pasted a stack trace, triage running twice because the engineer rephrased the question, triage running on a pasted log from staging, triage running because someone said the word "incident" while asking an unrelated question. Under the prompt design, you observe the workflow not running: low usage numbers, people reinventing it manually, and a support channel where the answer to "how do I do a triage?" is a screenshot of where the menu is.

The same capability, registered twice

Here is the tool version, in the Python SDK's FastMCP style:

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("incidents")

@mcp.tool()
def triage_incident(incident_id: str) -> str:
    """Run incident triage: gather recent deploys, summarise the
    failure signature, and open a postmortem document."""
    deploys = recent_deploys(incident_id)
    doc_url = create_postmortem_doc(incident_id, deploys)
    return f"Triage complete. Postmortem: {doc_url}"

And the prompt version of the same capability:

python
from mcp.server.fastmcp.prompts import base

@mcp.prompt(title="Run incident triage")
def triage_incident(incident_id: str) -> list[base.Message]:
    """Structure a triage for an open incident."""
    deploys = recent_deploys(incident_id)
    return [
        base.UserMessage(
            f"Incident {incident_id}. Deploys in the hour before the "
            f"first alert:\n\n{format_deploys(deploys)}\n\n"
            "Walk the timeline, name the most likely proximate cause, "
            "and list what we do not yet know."
        ),
        base.AssistantMessage(
            "I'll work through the timeline first. Before I do — has "
            "the incident been declared, and who is the incident lead?"
        ),
    ]

The two differ in more than their decorator. The tool creates the document; the prompt does not, because a prompt returns conversation, and the consequential step is left to a tool the user can watch fire. The tool's docstring is copy aimed at the model, phrased so it matches the right situations. The prompt's title is copy aimed at a person scanning a menu, and its docstring is a subtitle for that menu. The prompt can also seed an assistant turn, which the tool has no way to express, because a tool result is attributed to the tool rather than to the assistant.

ToolPrompt
Started bythe model, mid-turna person, before the turn
Where it appearsthe model's callable inventorythe host's interface — a menu, a slash command, a picker
Description is written forthe model, to match situationsa human, to be found and recognised
What comes backa tool result, attributed to the toolmessages with roles, placed in the conversation
Can fire with no human decisionyesno
Typical failure you observeit runs when it should not haveit never runs, because nobody found it
Where errors surfaceinside the model's turn, often recoverable by the modelin the host's UI, in front of the user who clicked

Eleven invocations is a small number and the interesting fact about it is not its size. All eleven are attributable to the two people who wrote the thing, which leaves nothing to explain: there is no residue of a few curious colleagues, no long tail of one-off tries, no group who found it and stopped. A usage figure with a tail would describe a workflow that people can find and mostly do not choose; a figure that resolves completely onto its authors describes one that nobody outside the room ever encountered. Those are different faults with different repairs, and the number alone does not separate them — the attribution does. It is also why the repair list in the paragraph above is a discovery list rather than a persuasion list.

The 4,200 has a property that matters more than its being wrong, which is that it was not checkable from where it was read. The page stated a result and did not state the window the result came from, so the two people who believed it on Monday morning could not have recomputed it even if it had occurred to them to try. That is a general hazard of a document assembled by something that reasons and then reports: the reasoning is discarded and the conclusion is kept, and the artefact is indistinguishable from one a person derived carefully. An impact figure produced this way is not merely more likely to be wrong than one an engineer wrote by hand; it is harder to audit, because the working was never anywhere.

Shipping both is not free coverage

The obvious move, once a team sees both failure modes, is to register the capability twice and let each path serve whoever wants it. It is worth being precise about what that costs, because the costs are not symmetrical with the benefits.

The security property collapses to the weaker of the two. If a capability is reachable as a tool, then it can start without a human decision, and the prompt entry alongside it does not restore anything. Any argument of the form "a person chose to start this workflow" is now false for the system as a whole, and remains false no matter how carefully the prompt path is built. The prompt becomes a convenience for people who like menus, and nothing more.

The two paths drift. They begin as one function with two decorators and separate within a quarter, because the prompt's messages get reworded after a user complains that the timeline section is confusing, and the tool's output does not, since no user ever sees it directly. Six months later the two produce materially different triages and nobody knows which one a given postmortem came from.

Telemetry stops being able to answer the question you will eventually be asked. "How many triages last quarter were started by a human?" requires correlating two event streams that were never designed to be joined, and the answer you produce will be an estimate with a caveat attached to it.

Hosts that surface both show the capability twice, in two places, under two slightly different names, and users pick whichever one they hit first. In practice that is the tool, because they are talking to the assistant already, which means the menu entry you built to give people an explicit start is the path almost nobody takes.

The security cost is the only one of those four that is not a matter of degree, and the reason is worth stating as the small piece of logic it is. "No invocation of this capability happens without a human decision" is a claim about every invocation, so a single invocation without a human decision refutes it. Registering the capability as a tool creates exactly such a path, one on which the model initiates it. The prompt path continues to have the property, and the property was never about a path — it was about the capability — so what you now hold is a true statement about one route and a false statement about the system. Drift, telemetry and duplicated menu entries all get worse gradually and can be argued about in terms of how much; this one is already at its worst on the day both decorators ship.

There are cases where two entries genuinely make sense, and they are cases where the two do different things — a triage_incident prompt that structures a human-led investigation, alongside a narrow fetch_deploys tool the model calls during it. The test is whether the tool would be a reasonable thing to expose on its own to a model with no user watching. "Open a postmortem document that other people will read and act on" does not pass that test. "List deploys in a time window" does.

Where the distinction stops helping

A prompt is not a system prompt. Nothing about it is persistent or hidden; it produces ordinary conversation turns at the moment it is invoked, and the user can see them, edit them in many hosts, and delete them. If you need instructions that apply to every turn of every session, prompts are the wrong place, and MCP does not offer you a right place — that is the host's business.

A prompt is also not a tool that returns text. That framing is common and it survives contact with the API, because prompts/get does return something text-shaped and you can imagine a tool doing the same. It fails on the property above: no arrangement of a tool's return type changes the fact that the model can call it. If the difference were about return types, the specification would not have needed a second primitive with its own capability flag, its own list method, and its own place in the host's interface.

Following the ordering makes it concrete. What determines whether the model can call something is whether its name and description are in the inventory the host sends to the provider, and that inventory is assembled before the model is invoked. The return type is a fact about what happens after a call has already been decided on. So no arrangement of the return shape can reach backwards into the moment the decision was made — the two live on opposite sides of the request. This is also why "make the tool return conversation-shaped messages" does not recreate a prompt, and why "make the prompt return a plain string" does not turn one into a tool.

What the prompt design does not buy you is reliability. If a workflow must run every time a page fires, a user-controlled primitive is exactly the wrong choice, and so is a model-controlled one; that is a job for a scheduler or an alert handler on your side of the wire, which then talks to the model rather than waiting to be talked to. Prompts are for the workflows where a person deciding to start one is the point.

Nor does it protect what happens after the messages land. A prompt returns text into a conversation with a model that has tools, and text is how you tell a model to use tools. That is the whole of the mechanism, and it is the reason the later chapters spend time on what goes into those messages.

Choosing between the two primitives is therefore a decision about initiation and nothing else, made once per capability, and it is worth making it deliberately because the two failure modes land on different people. A tool that fires on its own reading of the conversation costs you the incidents you did not choose, and their blast radius is whatever the capability does. A prompt that nobody finds costs you the usage, and usage is recoverable. Where a capability opens documents other people act on, or writes to a system anybody else reads, the question to ask is not which design is more convenient but whether it would be acceptable for it to run with nobody watching.

What is not yet settled is what actually crosses the wire when a person clicks the menu entry — what the client asks for, what the server has to answer with, and which of the two strings in that listing the client is supposed to send back.

Prompts — read the current version at https://daviddominguez.dev/learn/en/prompts/user-controlled-prompts

Back to top