You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 5908286
Browse filesBrowse the repository at this point in the historyBrowse files
docs: rich prompt content and runtime prompt changes on the prompts page
Two new sections on the prompts page. "More than text" shows a prompt
embedding the team style guide as an EmbeddedResource (registered as a
resource so the URI resolves) and attaching an image with
UserMessage(Image(...)), plus the paste-vs-embed judgment call and the
bare-content return form. "Changing the list at runtime" registers a
user-defined prompt from a tool with add_prompt(Prompt.from_function(...))
and announces it with ctx.notify_prompts_changed() and
ctx.session.send_prompt_list_changed(), with the listen frame, the legacy
frame and the duplicate-name behaviour. tutorial002 now imports the
message classes from mcp.server.mcpserver like the new examples.
No-Verification-Needed: docs, docs_src examples and tests only
Copy file name to clipboardExpand all lines: docs/servers/prompts.md
+157-2Lines changed: 157 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -85,7 +85,7 @@ Return a list of messages instead of a `str`:
85
85
--8<--"docs_src/prompts/tutorial002.py"
86
86
```
87
87
88
-
*`UserMessage` and `AssistantMessage` come from `mcp.server.mcpserver.prompts.base`. Hand them a `str` and they wrap it in `TextContent` for you. The role is the class name.
88
+
*`UserMessage` and `AssistantMessage` come from `mcp.server.mcpserver`. Hand them a `str` and they wrap it in `TextContent` for you. The role is the class name.
89
89
*`Message` is their common base. Use it as the return annotation.
90
90
91
91
Rendering `debug_error` now produces three messages, in order:
@@ -134,17 +134,172 @@ The `prompts/list` entry now carries everything a client needs to draw a good fo
134
134
```
135
135
136
136
!!! info
137
-
If you have read **[Tools](tools.md)**, you already know everything on this page. Same decorator, same
137
+
If you have read **[Tools](tools.md)**, you already know everything up to this point. Same decorator, same
138
138
docstring-as-description, same `Annotated`/`Field`. The only things that change are who
139
139
triggers it (the user) and where the result goes (into the conversation).
140
140
141
+
## More than text
142
+
143
+
A prompt message does not have to be text. Wherever `UserMessage` and `AssistantMessage` (the message classes from `mcp.server.mcpserver`) take a `str`, they also take a **content block**: any of the kinds a tool result can carry, which **[Images, audio & icons](media.md)** shows a tool producing one by one. This section uses two: an **embedded resource**, which carries a whole document along with its URI and MIME type, and an **image**.
144
+
145
+
### Embedding a file
146
+
147
+
A code review goes better when the reviewer has the team's style guide open. The Code Helper team keeps theirs in `style-guide.md`, next to `server.py`. Hand it to the model as a message of its own, ahead of the code:
148
+
149
+
```python title="server.py" hl_lines="5 12 21 23"
150
+
--8<--"docs_src/prompts/tutorial004.py"
151
+
```
152
+
153
+
*`@mcp.resource(...)` publishes the file at `style://python`, the way **[Resources](resources.md)** describes. Embedding does not depend on this line, but with it the URI inside the message is one a client can actually read back.
154
+
*`TextResourceContents` is what a client receives when it reads a text resource: a `uri`, a `mime_type`, and the `text`. Here the `text` comes from calling `style_guide()` directly (the decorator hands your function back unchanged), so the resource and the prompt serve one file from one place, read fresh on every render.
155
+
*`EmbeddedResource(resource=...)` is the content block that carries the document, and `UserMessage` takes it in place of a `str`. A block has no role of its own; the message supplies one. `EmbeddedResource` and `TextResourceContents` are protocol types from `mcp.types`.
156
+
157
+
Put a `style-guide.md` beside `server.py` (two bullet points of Markdown will do), then render `review_code`. The guide arrives as the first message and the request that refers to it as the second:
158
+
159
+
```json
160
+
{
161
+
"description": "Review a piece of code against the team style guide.",
162
+
"messages": [
163
+
{
164
+
"role": "user",
165
+
"content": {
166
+
"type": "resource",
167
+
"resource": {
168
+
"uri": "style://python",
169
+
"mimeType": "text/markdown",
170
+
"text": "* Prefer early returns.\n* No bare `except:`.\n"
171
+
}
172
+
}
173
+
},
174
+
{
175
+
"role": "user",
176
+
"content": {
177
+
"type": "text",
178
+
"text": "Review this code against the style guide above:\n\ndef add(a, b): return a + b"
179
+
}
180
+
}
181
+
],
182
+
"resultType": "complete"
183
+
}
184
+
```
185
+
186
+
You could paste the guide into the second message's f-string and the model would read the same words. Embedding it tells the client that this message is a document, with a URI and a format, so the client can show it as an attachment rather than a screenful of prose in the user's turn, and can read `style://python` again later. The model receives the file verbatim rather than folded into a sentence, and because the copy travels inside the message there is no `resources/read` round trip. Keep instructions in strings; send reference material (a style guide, a schema, the file under review) as an embedded resource.
187
+
188
+
A binary document embeds the same way with `BlobResourceContents` (also in `mcp.types`) in place of `TextResourceContents`: base64-encode the bytes into its `blob` field with `base64.b64encode(data).decode()` and give it the real `mime_type` (`application/pdf`, say). The spec requires a URI and a MIME type on every embedded resource; the SDK types do not enforce `mime_type`, so setting it is yours to remember. For a picture the model should look at, an image block (below) is the better fit.
189
+
190
+
!!! info
191
+
An embedded resource is a copy. When the document is large, or the client may not want it
192
+
at all, send a pointer instead: `ResourceLink(name="style_guide", uri="style://python",
193
+
mime_type="text/markdown")`, also from `mcp.types`, goes into a `UserMessage` the same way.
194
+
Nothing is embedded; the client reads the URI with `resources/read` if and when it wants the
195
+
contents, which only works when the URI really is one of your resources.
196
+
197
+
### Attaching an image
198
+
199
+
A message can also carry a picture, as an `ImageContent` block, for a model that accepts images. The Code Helper project keeps its architecture diagram beside the code as `architecture.png`:
200
+
201
+
```python title="server.py" hl_lines="4 15"
202
+
--8<--"docs_src/prompts/tutorial005.py"
203
+
```
204
+
205
+
*`Image` is the helper from **[Images, audio & icons](media.md)**. Give it `path=` and the MIME type is guessed from the suffix (`.png` is announced as `image/png`); for bytes already in memory, give it `data=` plus an explicit `format=`.
206
+
*`UserMessage` converts it: the file is read and base64-encoded when the prompt renders, and the message carries the protocol's `ImageContent`. `Audio(...)` becomes an `AudioContent` the same way.
207
+
*`component` is an ordinary prompt argument, and prompt arguments are always strings. A user cannot pass a picture in through one, so the image is something the server already has (a file on disk, a chart it just rendered) and the argument only supplies the words.
208
+
209
+
Any PNG named `architecture.png` beside `server.py` works. The client receives the diagram, then the question:
210
+
211
+
```json
212
+
{
213
+
"description": "Explain one component using the architecture diagram.",
"text": "Where does the payment gateway sit in this architecture, and what does it talk to?"
224
+
}
225
+
}
226
+
],
227
+
"resultType": "complete"
228
+
}
229
+
```
230
+
231
+
`data` is the whole file, base64-encoded (cut short here).
232
+
233
+
!!! tip
234
+
When every message is from the user, you can skip the wrappers: a prompt function may return
235
+
an `Image`, a content block, or a list mixing them with strings, and each item becomes one
236
+
user message, in order. `return [Image(path=DIAGRAM_FILE), f"Where does {component} sit..."]`
237
+
renders exactly the two messages above. You only need `UserMessage` / `AssistantMessage`
238
+
when one of the messages is an `assistant` turn.
239
+
240
+
A client needs nothing extra to receive these. In the Python `Client`, the `content` of each message in `get_prompt(...)`'s `messages` is typed as the same content-block union a tool result uses (`TextContent`, `ImageContent`, `AudioContent`, `ResourceLink`, `EmbeddedResource`). Narrow with `isinstance(block, EmbeddedResource)` or `isinstance(block, ImageContent)` before touching `.resource` or `.data`, as **[The Client](../client/index.md)** does with tool results.
241
+
242
+
## Changing the list at runtime
243
+
244
+
A decorated prompt exists from the moment the module is imported. The menu can also change while clients are connected: a user who keeps typing the same instruction ("explain this, line by line") wants it as an entry of its own, next to `review_code`.
245
+
246
+
Saving one is something the model does when the user asks, which makes it a tool. The tool does two things: register a prompt while the server is running, then tell connected clients that the list changed.
247
+
248
+
```python title="server.py" hl_lines="3 21-23"
249
+
--8<--"docs_src/prompts/tutorial006.py"
250
+
```
251
+
252
+
*`mcp.add_prompt(...)` takes a `Prompt` object, not a bare function. `Prompt.from_function` builds one from `template` exactly as `@mcp.prompt()` would, arguments included, so the new entry gets one required `code` argument. Pass `name=` and `description=`: left to default they come from the function, and every function this tool defines is called `template` and has no docstring.
253
+
*`ctx` is the parameter annotated `Context`. The SDK injects it and the model never sees it: `save_template`'s input schema has two properties, `name` and `instruction`. **[The Context](../handlers/context.md)** is the page about that object.
254
+
*`await ctx.notify_prompts_changed()` publishes the change to every `2026-07-28` client whose `subscriptions/listen` stream asked for prompt-list changes. With no such stream open it is a no-op, so there is nothing to check before calling it. **[Subscriptions](../handlers/subscriptions.md)** under *Inside your handler* is the page on those streams.
255
+
*`await ctx.session.send_prompt_list_changed()` sends the same notification to the client that made this call, if it is a pre-2026 client, down the stream its session keeps open. Other pre-2026 sessions are not told; they see the change on their next `prompts/list`. Neither line reaches the other era's clients, and neither raises when there is nobody to tell, so call both. **[Serving legacy clients](../run/legacy-clients.md)** explains the split.
256
+
257
+
Call `save_template` with `name="explain"` and `instruction="Explain what this code does, line by line."`. `prompts/list` includes `explain` the moment `add_prompt` returns; the notification exists only to tell clients that are already connected to ask again. On a `subscriptions/listen` stream it is one frame:
It names no prompt, only the stream it arrived on. A pre-2026 client receives the bare `{"method": "notifications/prompts/list_changed"}` on its session's stream instead. Either way the client calls `prompts/list` again and finds the new entry beside `review_code`:
265
+
266
+
```json
267
+
{
268
+
"name": "explain",
269
+
"description": "Explain what this code does, line by line.",
270
+
"arguments": [
271
+
{"name": "code", "required": true}
272
+
]
273
+
}
274
+
```
275
+
276
+
In the Python `Client`, the `subscriptions/listen` stream is `async with client.listen(prompts_list_changed=True) as sub:` and each change arrives as a `PromptsListChanged` event; **[Subscriptions](../client/subscriptions.md)** under *Clients* covers that end.
277
+
278
+
!!! check
279
+
Save `explain` a second time with different wording. The tool answers
280
+
`Saved 'explain' to the prompt menu.` again, `prompts/list` still shows the first description,
281
+
`prompts/get` still renders the first text, and the server logs one warning:
282
+
283
+
```text
284
+
Prompt already exists: explain
285
+
```
286
+
287
+
`add_prompt` keeps the entry it has and ignores the new one. `mcp.remove_prompt(name)` is its
288
+
reverse and wants the same two notification calls after it; unlike `add_prompt` it raises
289
+
`ValueError` for a name it does not have. Replacing a template is remove (if it exists), then
290
+
add, then notify.
291
+
292
+
Publish when the *menu* changes: an entry added, removed or replaced. When only the text behind an existing entry changes there is nothing to announce, because `prompts/get` renders it fresh every time.
293
+
141
294
## Recap
142
295
143
296
*`@mcp.prompt()` on a function makes it a prompt. Name from the function, description from the docstring.
144
297
* Prompts are **user-controlled**: the client lists them, the user picks one and fills in the arguments.
145
298
* Arguments are a flat list of named strings (no schema). A parameter with a default is optional.
146
299
* Return a `str` and it becomes one user message. Return a list of `UserMessage` / `AssistantMessage` to seed a multi-turn conversation.
147
300
*`title=` and `Field(description=...)` are what a client puts in its UI.
301
+
* A message is not limited to text. Wrap an `EmbeddedResource` in a `UserMessage` to attach a document with its URI and MIME type, or an `Image` to attach a picture.
148
302
* A missing required argument fails the whole request. There is no per-prompt error result.
303
+
* The menu can change while clients are connected: `mcp.add_prompt(Prompt.from_function(...))` or `mcp.remove_prompt(name)`, then `await ctx.notify_prompts_changed()` for listening `2026-07-28` clients and `await ctx.session.send_prompt_list_changed()` for a pre-2026 caller.
149
304
150
305
Server-side autocomplete for a prompt's (or a resource template's) arguments is **[Completions](completions.md)**.
0 commit comments