Skip to content

Commit 2251112

Browse files
committed
Prompt functions may return bare content blocks, Image or Audio
render() special-cased str and JSON-dumped anything else that was not a Message or dict, so a prompt returning Image(...) or a ready-made content block (or a list mixing captions and images) reached the client as the object's repr or a JSON blob. Bare content now becomes one user message via UserMessage(msg), making Message.__init__ the single place prompt content is coerced; the JSON-dump fallback for other values is unchanged. SyncPromptResult is widened to match.
1 parent 9cc83c9 commit 2251112

2 files changed

Lines changed: 31 additions & 5 deletions

File tree

src/mcp/server/mcpserver/prompts/base.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ def __init__(self, content: str | ContentBlock | Image | Audio, **kwargs: Any):
6262

6363
message_validator = TypeAdapter[UserMessage | AssistantMessage](UserMessage | AssistantMessage)
6464

65-
SyncPromptResult = str | Message | dict[str, Any] | InputRequiredResult | Sequence[str | Message | dict[str, Any]]
65+
_PromptResultItem = str | ContentBlock | Image | Audio | Message | dict[str, Any]
66+
SyncPromptResult = _PromptResultItem | InputRequiredResult | Sequence[_PromptResultItem]
6667
PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
6768

6869

@@ -98,7 +99,7 @@ def from_function(
9899
"""Create a Prompt from a function.
99100
100101
The function can return:
101-
- A string (converted to a message)
102+
- A string, content block, `Image` or `Audio` (each becomes a user message)
102103
- A Message object
103104
- A dict (converted to a message)
104105
- A sequence of any of the above
@@ -192,9 +193,8 @@ async def render(
192193
messages.append(msg)
193194
elif isinstance(msg, dict):
194195
messages.append(message_validator.validate_python(msg))
195-
elif isinstance(msg, str):
196-
content = TextContent(type="text", text=msg)
197-
messages.append(UserMessage(content=content))
196+
elif isinstance(msg, str | ContentBlock | Image | Audio): # bare content is one user message
197+
messages.append(UserMessage(msg))
198198
else: # pragma: no cover
199199
content = pydantic_core.to_json(msg, fallback=str, indent=2).decode()
200200
messages.append(Message(role="user", content=content))

tests/server/mcpserver/prompts/test_base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,29 @@ def deck(topic: str) -> list[_Slide]:
290290

291291
[listed] = await mcp.list_prompts()
292292
assert [arg.name for arg in listed.arguments or []] == ["topic"]
293+
294+
295+
_PNG = Image(data=b"img", format="png")
296+
_PNG_BLOCK = ImageContent(type="image", data="aW1n", mime_type="image/png")
297+
_DOC = EmbeddedResource(
298+
type="resource", resource=TextResourceContents(uri="file://notes.md", text="notes", mime_type="text/markdown")
299+
)
300+
301+
302+
@pytest.mark.anyio
303+
@pytest.mark.parametrize(
304+
("returned", "expected"),
305+
[
306+
(_PNG, [UserMessage(_PNG_BLOCK)]),
307+
(_DOC, [UserMessage(_DOC)]),
308+
(["Look at this:", _PNG], [UserMessage("Look at this:"), UserMessage(_PNG_BLOCK)]),
309+
],
310+
)
311+
async def test_bare_content_returned_from_a_prompt_becomes_user_messages(returned: Any, expected: list[Message]):
312+
"""SDK-defined: what a tool may return bare (a content block, `Image`, `Audio`), a prompt may too;
313+
each item becomes one user message instead of being JSON-dumped into text."""
314+
315+
def fn() -> Any:
316+
return returned
317+
318+
assert await Prompt.from_function(fn).render(None, Context()) == expected

0 commit comments

Comments
 (0)