Jeremiah Lowin commited on
Commit
3becd4d
·
1 Parent(s): 1cd0ac1

Ensure prompts return descriptions

Browse files
docs/clients/client.mdx CHANGED
@@ -104,6 +104,10 @@ You can make multiple calls to the server within the same `async with` block usi
104
 
105
  The `Client` provides methods corresponding to standard MCP requests:
106
 
 
 
 
 
107
  #### Tool Operations
108
 
109
  * **`list_tools()`**: Retrieves a list of tools available on the server.
@@ -153,6 +157,10 @@ The `Client` provides methods corresponding to standard MCP requests:
153
 
154
  The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly.
155
 
 
 
 
 
156
  ```python
157
  # Standard method - returns just the list of tools
158
  tools = await client.list_tools()
 
104
 
105
  The `Client` provides methods corresponding to standard MCP requests:
106
 
107
+ <Warning>
108
+ The standard client methods return user-friendly representations that may change as the protocol evolves. For consistent access to the complete data structure, use the `*_mcp` methods described later.
109
+ </Warning>
110
+
111
  #### Tool Operations
112
 
113
  * **`list_tools()`**: Retrieves a list of tools available on the server.
 
157
 
158
  The FastMCP client attempts to provide a "friendly" interface to the MCP protocol, but sometimes you may need access to the raw MCP protocol objects. Each of the main client methods that returns data has a corresponding `*_mcp` method that returns the raw MCP protocol objects directly.
159
 
160
+ <Warning>
161
+ The standard client methods (without `_mcp`) return user-friendly representations of MCP data, while `*_mcp` methods will always return the complete MCP protocol objects. As the protocol evolves, changes to these user-friendly representations may occur and could potentially be breaking. If you need consistent, stable access to the full data structure, prefer using the `*_mcp` methods.
162
+ </Warning>
163
+
164
  ```python
165
  # Standard method - returns just the list of tools
166
  tools = await client.list_tools()
docs/servers/prompts.mdx CHANGED
@@ -28,11 +28,11 @@ The most common way to define a prompt is by decorating a Python function. The d
28
 
29
  ```python
30
  from fastmcp import FastMCP
31
- from fastmcp.prompts.prompt import UserMessage, AssistantMessage, Message
32
 
33
  mcp = FastMCP(name="PromptServer")
34
 
35
- # Basic prompt returning a string (converted to UserMessage)
36
  @mcp.prompt()
37
  def ask_about_topic(topic: str) -> str:
38
  """Generates a user message asking for an explanation of a topic."""
@@ -40,10 +40,10 @@ def ask_about_topic(topic: str) -> str:
40
 
41
  # Prompt returning a specific message type
42
  @mcp.prompt()
43
- def generate_code_request(language: str, task_description: str) -> UserMessage:
44
  """Generates a user message requesting code generation."""
45
  content = f"Write a {language} function that performs the following task: {task_description}"
46
- return UserMessage(content=content)
47
  ```
48
 
49
  **Key Concepts:**
@@ -61,24 +61,21 @@ Functions with `*args` or `**kwargs` are not supported as prompts. This restrict
61
 
62
  FastMCP intelligently handles different return types from your prompt function:
63
 
64
- - **`str`**: Automatically converted to a single `UserMessage`.
65
- - **`Message`** (e.g., `UserMessage`, `AssistantMessage`): Used directly as provided.
66
- - **`dict`**: Parsed as a `Message` object if it has the correct structure.
67
- - **`list[Message]`**: Used as a sequence of messages (a conversation).
68
 
69
  ```python
 
 
70
  @mcp.prompt()
71
  def roleplay_scenario(character: str, situation: str) -> list[Message]:
72
  """Sets up a roleplaying scenario with initial messages."""
73
  return [
74
- UserMessage(f"Let's roleplay. You are {character}. The situation is: {situation}"),
75
- AssistantMessage("Okay, I understand. I am ready. What happens next?")
76
  ]
77
-
78
- @mcp.prompt()
79
- def ask_for_feedback() -> dict:
80
- """Generates a user message asking for feedback."""
81
- return {"role": "user", "content": "What did you think of my previous response?"}
82
  ```
83
 
84
  ### Type Annotations
 
28
 
29
  ```python
30
  from fastmcp import FastMCP
31
+ from fastmcp.prompts.prompt import Message, PromptMessage, TextContent
32
 
33
  mcp = FastMCP(name="PromptServer")
34
 
35
+ # Basic prompt returning a string (converted to user message automatically)
36
  @mcp.prompt()
37
  def ask_about_topic(topic: str) -> str:
38
  """Generates a user message asking for an explanation of a topic."""
 
40
 
41
  # Prompt returning a specific message type
42
  @mcp.prompt()
43
+ def generate_code_request(language: str, task_description: str) -> PromptMessage:
44
  """Generates a user message requesting code generation."""
45
  content = f"Write a {language} function that performs the following task: {task_description}"
46
+ return PromptMessage(role="user", content=TextContent(type="text", text=content))
47
  ```
48
 
49
  **Key Concepts:**
 
61
 
62
  FastMCP intelligently handles different return types from your prompt function:
63
 
64
+ - **`str`**: Automatically converted to a single `PromptMessage`.
65
+ - **`PromptMessage`**: Used directly as provided. (Note a more user-friendly `Message` constructor is available that can accept raw strings instead of `TextContent` objects.)
66
+ - **`list[PromptMessage | str]`**: Used as a sequence of messages (a conversation).
67
+ - **`Any`**: If the return type is not one of the above, the return value is attempted to be converted to a string and used as a `PromptMessage`.
68
 
69
  ```python
70
+ from fastmcp.prompts.prompt import Message
71
+
72
  @mcp.prompt()
73
  def roleplay_scenario(character: str, situation: str) -> list[Message]:
74
  """Sets up a roleplaying scenario with initial messages."""
75
  return [
76
+ Message(f"Let's roleplay. You are {character}. The situation is: {situation}"),
77
+ Message("Okay, I understand. I am ready. What happens next?", role="assistant")
78
  ]
 
 
 
 
 
79
  ```
80
 
81
  ### Type Annotations
src/fastmcp/client/client.py CHANGED
@@ -286,7 +286,7 @@ class Client:
286
 
287
  async def get_prompt(
288
  self, name: str, arguments: dict[str, str] | None = None
289
- ) -> list[mcp.types.PromptMessage]:
290
  """Retrieve a rendered prompt message list from the server.
291
 
292
  Args:
@@ -294,13 +294,14 @@ class Client:
294
  arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
295
 
296
  Returns:
297
- list[mcp.types.PromptMessage]: A list of prompt messages.
 
298
 
299
  Raises:
300
  RuntimeError: If called while the client is not connected.
301
  """
302
  result = await self.get_prompt_mcp(name=name, arguments=arguments)
303
- return result.messages
304
 
305
  # --- Completion ---
306
 
 
286
 
287
  async def get_prompt(
288
  self, name: str, arguments: dict[str, str] | None = None
289
+ ) -> mcp.types.GetPromptResult:
290
  """Retrieve a rendered prompt message list from the server.
291
 
292
  Args:
 
294
  arguments (dict[str, str] | None, optional): Arguments to pass to the prompt. Defaults to None.
295
 
296
  Returns:
297
+ mcp.types.GetPromptResult: The complete response object from the protocol,
298
+ containing the prompt messages and any additional metadata.
299
 
300
  Raises:
301
  RuntimeError: If called while the client is not connected.
302
  """
303
  result = await self.get_prompt_mcp(name=name, arguments=arguments)
304
+ return result
305
 
306
  # --- Completion ---
307
 
src/fastmcp/prompts/__init__.py CHANGED
@@ -1,4 +1,9 @@
1
- from .prompt import Prompt, Message, UserMessage, AssistantMessage
2
  from .prompt_manager import PromptManager
3
 
4
- __all__ = ["Prompt", "PromptManager", "Message", "UserMessage", "AssistantMessage"]
 
 
 
 
 
 
1
+ from .prompt import Prompt, PromptMessage, Message
2
  from .prompt_manager import PromptManager
3
 
4
+ __all__ = [
5
+ "Prompt",
6
+ "PromptManager",
7
+ "PromptMessage",
8
+ "Message",
9
+ ]
src/fastmcp/prompts/prompt.py CHANGED
@@ -4,10 +4,10 @@ from __future__ import annotations as _annotations
4
 
5
  import inspect
6
  from collections.abc import Awaitable, Callable, Sequence
7
- from typing import TYPE_CHECKING, Annotated, Any, Literal
8
 
9
  import pydantic_core
10
- from mcp.types import EmbeddedResource, ImageContent, TextContent
11
  from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
@@ -28,32 +28,24 @@ if TYPE_CHECKING:
28
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
29
 
30
 
31
- class Message(BaseModel):
32
- """Base class for all prompt messages."""
 
 
 
 
 
 
 
33
 
34
- role: Literal["user", "assistant"]
35
- content: CONTENT_TYPES
36
 
37
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
38
- if isinstance(content, str):
39
- content = TextContent(type="text", text=content)
40
- super().__init__(content=content, **kwargs)
41
-
42
-
43
- def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
44
- """A message from the user."""
45
- return Message(content=content, role="user", **kwargs)
46
-
47
-
48
- def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
49
- """A message from the assistant."""
50
- return Message(content=content, role="assistant", **kwargs)
51
-
52
-
53
- message_validator = TypeAdapter[Message](Message)
54
 
55
  SyncPromptResult = (
56
- str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
 
 
 
57
  )
58
  PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
59
 
@@ -156,7 +148,7 @@ class Prompt(BaseModel):
156
  self,
157
  arguments: dict[str, Any] | None = None,
158
  context: Context[ServerSessionT, LifespanContextT] | None = None,
159
- ) -> list[Message]:
160
  """Render the prompt with arguments."""
161
  # Validate required arguments
162
  if self.arguments:
@@ -182,21 +174,28 @@ class Prompt(BaseModel):
182
  result = [result]
183
 
184
  # Convert result to messages
185
- messages: list[Message] = []
186
- for msg in result: # type: ignore[reportUnknownVariableType]
187
  try:
188
- if isinstance(msg, Message):
189
  messages.append(msg)
190
- elif isinstance(msg, dict):
191
- messages.append(message_validator.validate_python(msg))
192
  elif isinstance(msg, str):
193
- content = TextContent(type="text", text=msg)
194
- messages.append(Message(role="user", content=content))
 
 
 
 
195
  else:
196
  content = pydantic_core.to_json(
197
  msg, fallback=str, indent=2
198
  ).decode()
199
- messages.append(Message(role="user", content=content))
 
 
 
 
 
200
  except Exception:
201
  raise ValueError(
202
  f"Could not convert prompt result to message: {msg}"
 
4
 
5
  import inspect
6
  from collections.abc import Awaitable, Callable, Sequence
7
+ from typing import TYPE_CHECKING, Annotated, Any
8
 
9
  import pydantic_core
10
+ from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
11
  from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
 
28
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
29
 
30
 
31
+ def Message(
32
+ content: str | CONTENT_TYPES, role: Role | None = None, **kwargs: Any
33
+ ) -> PromptMessage:
34
+ """A user-friendly constructor for PromptMessage."""
35
+ if isinstance(content, str):
36
+ content = TextContent(type="text", text=content)
37
+ if role is None:
38
+ role = "user"
39
+ return PromptMessage(content=content, role=role, **kwargs)
40
 
 
 
41
 
42
+ message_validator = TypeAdapter[PromptMessage](PromptMessage)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  SyncPromptResult = (
45
+ str
46
+ | PromptMessage
47
+ | dict[str, Any]
48
+ | Sequence[str | PromptMessage | dict[str, Any]]
49
  )
50
  PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
51
 
 
148
  self,
149
  arguments: dict[str, Any] | None = None,
150
  context: Context[ServerSessionT, LifespanContextT] | None = None,
151
+ ) -> list[PromptMessage]:
152
  """Render the prompt with arguments."""
153
  # Validate required arguments
154
  if self.arguments:
 
174
  result = [result]
175
 
176
  # Convert result to messages
177
+ messages: list[PromptMessage] = []
178
+ for msg in result:
179
  try:
180
+ if isinstance(msg, PromptMessage):
181
  messages.append(msg)
 
 
182
  elif isinstance(msg, str):
183
+ messages.append(
184
+ PromptMessage(
185
+ role="user",
186
+ content=TextContent(type="text", text=msg),
187
+ )
188
+ )
189
  else:
190
  content = pydantic_core.to_json(
191
  msg, fallback=str, indent=2
192
  ).decode()
193
+ messages.append(
194
+ PromptMessage(
195
+ role="user",
196
+ content=TextContent(type="text", text=content),
197
+ )
198
+ )
199
  except Exception:
200
  raise ValueError(
201
  f"Could not convert prompt result to message: {msg}"
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -5,8 +5,10 @@ from __future__ import annotations as _annotations
5
  from collections.abc import Awaitable, Callable
6
  from typing import TYPE_CHECKING, Any
7
 
 
 
8
  from fastmcp.exceptions import NotFoundError
9
- from fastmcp.prompts.prompt import Message, Prompt, PromptResult
10
  from fastmcp.settings import DuplicateBehavior
11
  from fastmcp.utilities.logging import get_logger
12
 
@@ -81,13 +83,18 @@ class PromptManager:
81
  name: str,
82
  arguments: dict[str, Any] | None = None,
83
  context: Context[ServerSessionT, LifespanContextT] | None = None,
84
- ) -> list[Message]:
85
  """Render a prompt by name with arguments."""
86
  prompt = self.get_prompt(name)
87
  if not prompt:
88
  raise NotFoundError(f"Unknown prompt: {name}")
89
 
90
- return await prompt.render(arguments, context=context)
 
 
 
 
 
91
 
92
  def has_prompt(self, key: str) -> bool:
93
  """Check if a prompt exists."""
 
5
  from collections.abc import Awaitable, Callable
6
  from typing import TYPE_CHECKING, Any
7
 
8
+ from mcp import GetPromptResult
9
+
10
  from fastmcp.exceptions import NotFoundError
11
+ from fastmcp.prompts.prompt import Prompt, PromptResult
12
  from fastmcp.settings import DuplicateBehavior
13
  from fastmcp.utilities.logging import get_logger
14
 
 
83
  name: str,
84
  arguments: dict[str, Any] | None = None,
85
  context: Context[ServerSessionT, LifespanContextT] | None = None,
86
+ ) -> GetPromptResult:
87
  """Render a prompt by name with arguments."""
88
  prompt = self.get_prompt(name)
89
  if not prompt:
90
  raise NotFoundError(f"Unknown prompt: {name}")
91
 
92
+ messages = await prompt.render(arguments, context=context)
93
+
94
+ return GetPromptResult(
95
+ description=prompt.description,
96
+ messages=messages,
97
+ )
98
 
99
  def has_prompt(self, key: str) -> bool:
100
  """Check if a prompt exists."""
src/fastmcp/server/proxy.py CHANGED
@@ -19,7 +19,7 @@ from pydantic.networks import AnyUrl
19
 
20
  from fastmcp.client import Client
21
  from fastmcp.exceptions import NotFoundError
22
- from fastmcp.prompts import Message, Prompt
23
  from fastmcp.resources import Resource, ResourceTemplate
24
  from fastmcp.server.context import Context
25
  from fastmcp.server.server import FastMCP
@@ -175,10 +175,10 @@ class ProxyPrompt(Prompt):
175
  self,
176
  arguments: dict[str, Any],
177
  context: Context[ServerSessionT, LifespanContextT] | None = None,
178
- ) -> list[Message]:
179
  async with self._client:
180
  result = await self._client.get_prompt(self.name, arguments)
181
- return [Message(role=m.role, content=m.content) for m in result]
182
 
183
 
184
  class FastMCPProxy(FastMCP):
@@ -291,4 +291,4 @@ class FastMCPProxy(FastMCP):
291
  except NotFoundError:
292
  async with self.client:
293
  result = await self.client.get_prompt(name, arguments)
294
- return GetPromptResult(messages=result)
 
19
 
20
  from fastmcp.client import Client
21
  from fastmcp.exceptions import NotFoundError
22
+ from fastmcp.prompts import Prompt, PromptMessage
23
  from fastmcp.resources import Resource, ResourceTemplate
24
  from fastmcp.server.context import Context
25
  from fastmcp.server.server import FastMCP
 
175
  self,
176
  arguments: dict[str, Any],
177
  context: Context[ServerSessionT, LifespanContextT] | None = None,
178
+ ) -> list[PromptMessage]:
179
  async with self._client:
180
  result = await self._client.get_prompt(self.name, arguments)
181
+ return result.messages
182
 
183
 
184
  class FastMCPProxy(FastMCP):
 
291
  except NotFoundError:
292
  async with self.client:
293
  result = await self.client.get_prompt(name, arguments)
294
+ return result
src/fastmcp/server/server.py CHANGED
@@ -32,7 +32,6 @@ from mcp.types import (
32
  EmbeddedResource,
33
  GetPromptResult,
34
  ImageContent,
35
- PromptMessage,
36
  TextContent,
37
  ToolAnnotations,
38
  )
@@ -504,15 +503,10 @@ class FastMCP(Generic[LifespanResultT]):
504
  """
505
  if self._prompt_manager.has_prompt(name):
506
  context = self.get_context()
507
- messages = await self._prompt_manager.render_prompt(
508
  name, arguments=arguments or {}, context=context
509
  )
510
-
511
- return GetPromptResult(
512
- messages=[
513
- PromptMessage(role=m.role, content=m.content) for m in messages
514
- ]
515
- )
516
  else:
517
  for server in self._mounted_servers.values():
518
  if server.match_prompt(name):
 
32
  EmbeddedResource,
33
  GetPromptResult,
34
  ImageContent,
 
35
  TextContent,
36
  ToolAnnotations,
37
  )
 
503
  """
504
  if self._prompt_manager.has_prompt(name):
505
  context = self.get_context()
506
+ prompt_result = await self._prompt_manager.render_prompt(
507
  name, arguments=arguments or {}, context=context
508
  )
509
+ return prompt_result
 
 
 
 
 
510
  else:
511
  for server in self._mounted_servers.values():
512
  if server.match_prompt(name):
tests/client/test_client.py CHANGED
@@ -5,6 +5,7 @@ from pydantic import AnyUrl
5
 
6
  from fastmcp.client import Client
7
  from fastmcp.client.transports import FastMCPTransport
 
8
  from fastmcp.server.server import FastMCP
9
 
10
 
@@ -38,6 +39,7 @@ def fastmcp_server():
38
  # Add a prompt
39
  @server.prompt()
40
  def welcome(name: str) -> str:
 
41
  return f"Welcome to FastMCP, {name}!"
42
 
43
  return server
@@ -182,8 +184,9 @@ async def test_get_prompt(fastmcp_server):
182
  result = await client.get_prompt("welcome", {"name": "Developer"})
183
 
184
  # The result should contain our welcome message
185
- result_str = str(result)
186
- assert "Welcome to FastMCP, Developer!" in result_str
 
187
 
188
 
189
  async def test_get_prompt_mcp(fastmcp_server):
@@ -193,11 +196,10 @@ async def test_get_prompt_mcp(fastmcp_server):
193
  async with client:
194
  result = await client.get_prompt_mcp("welcome", {"name": "Developer"})
195
 
196
- # Check that we got the raw MCP GetPromptResult object
197
- assert hasattr(result, "messages")
198
- assert len(result.messages) > 0
199
- result_str = str(result.messages)
200
- assert "Welcome to FastMCP, Developer!" in result_str
201
 
202
 
203
  async def test_read_resource(fastmcp_server):
 
5
 
6
  from fastmcp.client import Client
7
  from fastmcp.client.transports import FastMCPTransport
8
+ from fastmcp.prompts.prompt import TextContent
9
  from fastmcp.server.server import FastMCP
10
 
11
 
 
39
  # Add a prompt
40
  @server.prompt()
41
  def welcome(name: str) -> str:
42
+ """Example greeting prompt."""
43
  return f"Welcome to FastMCP, {name}!"
44
 
45
  return server
 
184
  result = await client.get_prompt("welcome", {"name": "Developer"})
185
 
186
  # The result should contain our welcome message
187
+ assert isinstance(result.messages[0].content, TextContent)
188
+ assert result.messages[0].content.text == "Welcome to FastMCP, Developer!"
189
+ assert result.description == "Example greeting prompt."
190
 
191
 
192
  async def test_get_prompt_mcp(fastmcp_server):
 
196
  async with client:
197
  result = await client.get_prompt_mcp("welcome", {"name": "Developer"})
198
 
199
+ # The result should contain our welcome message
200
+ assert isinstance(result.messages[0].content, TextContent)
201
+ assert result.messages[0].content.text == "Welcome to FastMCP, Developer!"
202
+ assert result.description == "Example greeting prompt."
 
203
 
204
 
205
  async def test_read_resource(fastmcp_server):
tests/prompts/{test_base.py → test_prompt.py} RENAMED
@@ -3,11 +3,10 @@ from mcp.types import EmbeddedResource, TextResourceContents
3
  from pydantic import FileUrl
4
 
5
  from fastmcp.prompts.prompt import (
6
- AssistantMessage,
7
  Message,
8
  Prompt,
 
9
  TextContent,
10
- UserMessage,
11
  )
12
 
13
 
@@ -18,7 +17,9 @@ class TestRenderPrompt:
18
 
19
  prompt = Prompt.from_function(fn)
20
  assert await prompt.render() == [
21
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
 
 
22
  ]
23
 
24
  async def test_async_fn(self):
@@ -27,7 +28,9 @@ class TestRenderPrompt:
27
 
28
  prompt = Prompt.from_function(fn)
29
  assert await prompt.render() == [
30
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
 
 
31
  ]
32
 
33
  async def test_fn_with_args(self):
@@ -36,10 +39,11 @@ class TestRenderPrompt:
36
 
37
  prompt = Prompt.from_function(fn)
38
  assert await prompt.render(arguments=dict(name="World")) == [
39
- UserMessage(
 
40
  content=TextContent(
41
  type="text", text="Hello, World! You're 30 years old."
42
- )
43
  )
44
  ]
45
 
@@ -52,33 +56,42 @@ class TestRenderPrompt:
52
  await prompt.render(arguments=dict(age=40))
53
 
54
  async def test_fn_returns_message(self):
55
- async def fn() -> Message:
56
- return UserMessage(content="Hello, world!")
 
 
57
 
58
  prompt = Prompt.from_function(fn)
59
  assert await prompt.render() == [
60
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
 
 
61
  ]
62
 
63
  async def test_fn_returns_assistant_message(self):
64
- async def fn() -> Message:
65
- return AssistantMessage(
66
- content=TextContent(type="text", text="Hello, world!")
67
  )
68
 
69
  prompt = Prompt.from_function(fn)
70
  assert await prompt.render() == [
71
- AssistantMessage(content=TextContent(type="text", text="Hello, world!"))
 
 
72
  ]
73
 
74
  async def test_fn_returns_multiple_messages(self):
75
  expected = [
76
- UserMessage("Hello, world!"),
77
- AssistantMessage("How can I help you today?"),
78
- UserMessage("I'm looking for a restaurant in the center of town."),
 
 
 
79
  ]
80
 
81
- async def fn() -> list[Message]:
82
  return expected
83
 
84
  prompt = Prompt.from_function(fn)
@@ -94,13 +107,17 @@ class TestRenderPrompt:
94
  return expected
95
 
96
  prompt = Prompt.from_function(fn)
97
- assert await prompt.render() == [UserMessage(t) for t in expected]
 
 
 
98
 
99
  async def test_fn_returns_resource_content(self):
100
  """Test returning a message with resource content."""
101
 
102
- async def fn() -> Message:
103
- return UserMessage(
 
104
  content=EmbeddedResource(
105
  type="resource",
106
  resource=TextResourceContents(
@@ -108,12 +125,13 @@ class TestRenderPrompt:
108
  text="File contents",
109
  mimeType="text/plain",
110
  ),
111
- )
112
  )
113
 
114
  prompt = Prompt.from_function(fn)
115
  assert await prompt.render() == [
116
- UserMessage(
 
117
  content=EmbeddedResource(
118
  type="resource",
119
  resource=TextResourceContents(
@@ -121,17 +139,18 @@ class TestRenderPrompt:
121
  text="File contents",
122
  mimeType="text/plain",
123
  ),
124
- )
125
  )
126
  ]
127
 
128
  async def test_fn_returns_mixed_content(self):
129
  """Test returning messages with mixed content types."""
130
 
131
- async def fn() -> list[Message]:
132
  return [
133
- UserMessage(content="Please analyze this file:"),
134
- UserMessage(
 
135
  content=EmbeddedResource(
136
  type="resource",
137
  resource=TextResourceContents(
@@ -139,17 +158,19 @@ class TestRenderPrompt:
139
  text="File contents",
140
  mimeType="text/plain",
141
  ),
142
- )
143
  ),
144
- AssistantMessage(content="I'll help analyze that file."),
145
  ]
146
 
147
  prompt = Prompt.from_function(fn)
148
  assert await prompt.render() == [
149
- UserMessage(
150
- content=TextContent(type="text", text="Please analyze this file:")
 
151
  ),
152
- UserMessage(
 
153
  content=EmbeddedResource(
154
  type="resource",
155
  resource=TextResourceContents(
@@ -157,32 +178,34 @@ class TestRenderPrompt:
157
  text="File contents",
158
  mimeType="text/plain",
159
  ),
160
- )
161
  ),
162
- AssistantMessage(
163
- content=TextContent(type="text", text="I'll help analyze that file.")
 
164
  ),
165
  ]
166
 
167
- async def test_fn_returns_dict_with_resource(self):
168
  """Test returning a dict with resource content."""
169
 
170
- async def fn() -> dict:
171
- return {
172
- "role": "user",
173
- "content": {
174
- "type": "resource",
175
- "resource": {
176
- "uri": FileUrl("file://file.txt"),
177
- "text": "File contents",
178
- "mimeType": "text/plain",
179
- },
180
- },
181
- }
182
 
183
  prompt = Prompt.from_function(fn)
184
  assert await prompt.render() == [
185
- UserMessage(
 
186
  content=EmbeddedResource(
187
  type="resource",
188
  resource=TextResourceContents(
@@ -190,6 +213,6 @@ class TestRenderPrompt:
190
  text="File contents",
191
  mimeType="text/plain",
192
  ),
193
- )
194
  )
195
  ]
 
3
  from pydantic import FileUrl
4
 
5
  from fastmcp.prompts.prompt import (
 
6
  Message,
7
  Prompt,
8
+ PromptMessage,
9
  TextContent,
 
10
  )
11
 
12
 
 
17
 
18
  prompt = Prompt.from_function(fn)
19
  assert await prompt.render() == [
20
+ PromptMessage(
21
+ role="user", content=TextContent(type="text", text="Hello, world!")
22
+ )
23
  ]
24
 
25
  async def test_async_fn(self):
 
28
 
29
  prompt = Prompt.from_function(fn)
30
  assert await prompt.render() == [
31
+ PromptMessage(
32
+ role="user", content=TextContent(type="text", text="Hello, world!")
33
+ )
34
  ]
35
 
36
  async def test_fn_with_args(self):
 
39
 
40
  prompt = Prompt.from_function(fn)
41
  assert await prompt.render(arguments=dict(name="World")) == [
42
+ PromptMessage(
43
+ role="user",
44
  content=TextContent(
45
  type="text", text="Hello, World! You're 30 years old."
46
+ ),
47
  )
48
  ]
49
 
 
56
  await prompt.render(arguments=dict(age=40))
57
 
58
  async def test_fn_returns_message(self):
59
+ async def fn() -> PromptMessage:
60
+ return PromptMessage(
61
+ role="user", content=TextContent(type="text", text="Hello, world!")
62
+ )
63
 
64
  prompt = Prompt.from_function(fn)
65
  assert await prompt.render() == [
66
+ PromptMessage(
67
+ role="user", content=TextContent(type="text", text="Hello, world!")
68
+ )
69
  ]
70
 
71
  async def test_fn_returns_assistant_message(self):
72
+ async def fn() -> PromptMessage:
73
+ return PromptMessage(
74
+ role="assistant", content=TextContent(type="text", text="Hello, world!")
75
  )
76
 
77
  prompt = Prompt.from_function(fn)
78
  assert await prompt.render() == [
79
+ PromptMessage(
80
+ role="assistant", content=TextContent(type="text", text="Hello, world!")
81
+ )
82
  ]
83
 
84
  async def test_fn_returns_multiple_messages(self):
85
  expected = [
86
+ Message(role="user", content="Hello, world!"),
87
+ Message(role="assistant", content="How can I help you today?"),
88
+ Message(
89
+ role="user",
90
+ content="I'm looking for a restaurant in the center of town.",
91
+ ),
92
  ]
93
 
94
+ async def fn() -> list[PromptMessage]:
95
  return expected
96
 
97
  prompt = Prompt.from_function(fn)
 
107
  return expected
108
 
109
  prompt = Prompt.from_function(fn)
110
+ assert await prompt.render() == [
111
+ PromptMessage(role="user", content=TextContent(type="text", text=t))
112
+ for t in expected
113
+ ]
114
 
115
  async def test_fn_returns_resource_content(self):
116
  """Test returning a message with resource content."""
117
 
118
+ async def fn() -> PromptMessage:
119
+ return PromptMessage(
120
+ role="user",
121
  content=EmbeddedResource(
122
  type="resource",
123
  resource=TextResourceContents(
 
125
  text="File contents",
126
  mimeType="text/plain",
127
  ),
128
+ ),
129
  )
130
 
131
  prompt = Prompt.from_function(fn)
132
  assert await prompt.render() == [
133
+ PromptMessage(
134
+ role="user",
135
  content=EmbeddedResource(
136
  type="resource",
137
  resource=TextResourceContents(
 
139
  text="File contents",
140
  mimeType="text/plain",
141
  ),
142
+ ),
143
  )
144
  ]
145
 
146
  async def test_fn_returns_mixed_content(self):
147
  """Test returning messages with mixed content types."""
148
 
149
+ async def fn() -> list[PromptMessage | str]:
150
  return [
151
+ "Please analyze this file:",
152
+ PromptMessage(
153
+ role="user",
154
  content=EmbeddedResource(
155
  type="resource",
156
  resource=TextResourceContents(
 
158
  text="File contents",
159
  mimeType="text/plain",
160
  ),
161
+ ),
162
  ),
163
+ Message(role="assistant", content="I'll help analyze that file."),
164
  ]
165
 
166
  prompt = Prompt.from_function(fn)
167
  assert await prompt.render() == [
168
+ PromptMessage(
169
+ role="user",
170
+ content=TextContent(type="text", text="Please analyze this file:"),
171
  ),
172
+ PromptMessage(
173
+ role="user",
174
  content=EmbeddedResource(
175
  type="resource",
176
  resource=TextResourceContents(
 
178
  text="File contents",
179
  mimeType="text/plain",
180
  ),
181
+ ),
182
  ),
183
+ PromptMessage(
184
+ role="assistant",
185
+ content=TextContent(type="text", text="I'll help analyze that file."),
186
  ),
187
  ]
188
 
189
+ async def test_fn_returns_message_with_resource(self):
190
  """Test returning a dict with resource content."""
191
 
192
+ async def fn() -> PromptMessage:
193
+ return PromptMessage(
194
+ role="user",
195
+ content=EmbeddedResource(
196
+ type="resource",
197
+ resource=TextResourceContents(
198
+ uri=FileUrl("file://file.txt"),
199
+ text="File contents",
200
+ mimeType="text/plain",
201
+ ),
202
+ ),
203
+ )
204
 
205
  prompt = Prompt.from_function(fn)
206
  assert await prompt.render() == [
207
+ PromptMessage(
208
+ role="user",
209
  content=EmbeddedResource(
210
  type="resource",
211
  resource=TextResourceContents(
 
213
  text="File contents",
214
  mimeType="text/plain",
215
  ),
216
+ ),
217
  )
218
  ]
tests/prompts/test_prompt_manager.py CHANGED
@@ -7,7 +7,7 @@ from mcp.shared.context import LifespanContextT
7
  from fastmcp import Context
8
  from fastmcp.exceptions import NotFoundError
9
  from fastmcp.prompts import Prompt
10
- from fastmcp.prompts.prompt import TextContent, UserMessage
11
  from fastmcp.prompts.prompt_manager import PromptManager
12
 
13
 
@@ -147,28 +147,36 @@ class TestPromptManager:
147
  """Test rendering a prompt."""
148
 
149
  def fn() -> str:
 
150
  return "Hello, world!"
151
 
152
  manager = PromptManager()
153
  prompt = Prompt.from_function(fn)
154
  manager.add_prompt(prompt)
155
- messages = await manager.render_prompt("fn")
156
- assert messages == [
157
- UserMessage(content=TextContent(type="text", text="Hello, world!"))
 
 
 
158
  ]
159
 
160
  async def test_render_prompt_with_args(self):
161
  """Test rendering a prompt with arguments."""
162
 
163
  def fn(name: str) -> str:
 
164
  return f"Hello, {name}!"
165
 
166
  manager = PromptManager()
167
  prompt = Prompt.from_function(fn)
168
  manager.add_prompt(prompt)
169
- messages = await manager.render_prompt("fn", arguments={"name": "World"})
170
- assert messages == [
171
- UserMessage(content=TextContent(type="text", text="Hello, World!"))
 
 
 
172
  ]
173
 
174
  async def test_render_unknown_prompt(self):
 
7
  from fastmcp import Context
8
  from fastmcp.exceptions import NotFoundError
9
  from fastmcp.prompts import Prompt
10
+ from fastmcp.prompts.prompt import PromptMessage, TextContent
11
  from fastmcp.prompts.prompt_manager import PromptManager
12
 
13
 
 
147
  """Test rendering a prompt."""
148
 
149
  def fn() -> str:
150
+ """An example prompt."""
151
  return "Hello, world!"
152
 
153
  manager = PromptManager()
154
  prompt = Prompt.from_function(fn)
155
  manager.add_prompt(prompt)
156
+ result = await manager.render_prompt("fn")
157
+ assert result.description == "An example prompt."
158
+ assert result.messages == [
159
+ PromptMessage(
160
+ role="user", content=TextContent(type="text", text="Hello, world!")
161
+ )
162
  ]
163
 
164
  async def test_render_prompt_with_args(self):
165
  """Test rendering a prompt with arguments."""
166
 
167
  def fn(name: str) -> str:
168
+ """An example prompt."""
169
  return f"Hello, {name}!"
170
 
171
  manager = PromptManager()
172
  prompt = Prompt.from_function(fn)
173
  manager.add_prompt(prompt)
174
+ result = await manager.render_prompt("fn", arguments={"name": "World"})
175
+ assert result.description == "An example prompt."
176
+ assert result.messages == [
177
+ PromptMessage(
178
+ role="user", content=TextContent(type="text", text="Hello, World!")
179
+ )
180
  ]
181
 
182
  async def test_render_unknown_prompt(self):
tests/server/test_import_server.py CHANGED
@@ -319,14 +319,16 @@ async def test_import_with_proxy_prompts():
319
 
320
  @api_app.prompt()
321
  def greeting(name: str) -> str:
 
322
  return f"Hello, {name} from API!"
323
 
324
  proxy_app = FastMCP.from_client(Client(api_app))
325
  await main_app.import_server("api", proxy_app)
326
 
327
  result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"})
328
- assert result.messages is not None
329
- # Check that the message contains our greeting
 
330
 
331
 
332
  async def test_import_with_proxy_resources():
 
319
 
320
  @api_app.prompt()
321
  def greeting(name: str) -> str:
322
+ """Example greeting prompt."""
323
  return f"Hello, {name} from API!"
324
 
325
  proxy_app = FastMCP.from_client(Client(api_app))
326
  await main_app.import_server("api", proxy_app)
327
 
328
  result = await main_app._mcp_get_prompt("api_greeting", {"name": "World"})
329
+ assert isinstance(result.messages[0].content, TextContent)
330
+ assert result.messages[0].content.text == "Hello, World from API!"
331
+ assert result.description == "Example greeting prompt."
332
 
333
 
334
  async def test_import_with_proxy_resources():
tests/server/test_proxy.py CHANGED
@@ -207,7 +207,7 @@ class TestPrompts:
207
  async def test_render_prompt_calls_prompt(self, proxy_server):
208
  async with Client(proxy_server) as client:
209
  result = await client.get_prompt("welcome", {"name": "Alice"})
210
- assert isinstance(result[0], mcp.types.PromptMessage)
211
- assert result[0].role == "user"
212
- assert isinstance(result[0].content, mcp.types.TextContent)
213
- assert result[0].content.text == "Welcome to FastMCP, Alice!"
 
207
  async def test_render_prompt_calls_prompt(self, proxy_server):
208
  async with Client(proxy_server) as client:
209
  result = await client.get_prompt("welcome", {"name": "Alice"})
210
+ assert isinstance(result.messages[0], mcp.types.PromptMessage)
211
+ assert result.messages[0].role == "user"
212
+ assert isinstance(result.messages[0].content, mcp.types.TextContent)
213
+ assert result.messages[0].content.text == "Welcome to FastMCP, Alice!"
tests/server/test_server.py CHANGED
@@ -640,16 +640,16 @@ class TestPromptDecorator:
640
 
641
  async with Client(mcp) as client:
642
  result = await client.get_prompt("test_prompt", {"name": "World"})
643
- assert len(result) == 1
644
- message = result[0]
645
  assert isinstance(message.content, TextContent)
646
  assert message.content.text == "Hello, World!"
647
 
648
  result = await client.get_prompt(
649
  "test_prompt", {"name": "World", "greeting": "Hi"}
650
  )
651
- assert len(result) == 1
652
- message = result[0]
653
  assert isinstance(message.content, TextContent)
654
  assert message.content.text == "Hi, World!"
655
 
@@ -668,8 +668,8 @@ class TestPromptDecorator:
668
 
669
  async with Client(mcp) as client:
670
  result = await client.get_prompt("test_prompt")
671
- assert len(result) == 1
672
- message = result[0]
673
  assert isinstance(message.content, TextContent)
674
  assert message.content.text == "My prefix: Hello, world!"
675
 
@@ -687,8 +687,8 @@ class TestPromptDecorator:
687
 
688
  async with Client(mcp) as client:
689
  result = await client.get_prompt("test_prompt")
690
- assert len(result) == 1
691
- message = result[0]
692
  assert isinstance(message.content, TextContent)
693
  assert message.content.text == "Class prefix: Hello, world!"
694
 
@@ -703,8 +703,8 @@ class TestPromptDecorator:
703
 
704
  async with Client(mcp) as client:
705
  result = await client.get_prompt("test_prompt")
706
- assert len(result) == 1
707
- message = result[0]
708
  assert isinstance(message.content, TextContent)
709
  assert message.content.text == "Static Hello, world!"
710
 
@@ -717,8 +717,8 @@ class TestPromptDecorator:
717
 
718
  async with Client(mcp) as client:
719
  result = await client.get_prompt("test_prompt")
720
- assert len(result) == 1
721
- message = result[0]
722
  assert isinstance(message.content, TextContent)
723
  assert message.content.text == "Async Hello, world!"
724
 
 
640
 
641
  async with Client(mcp) as client:
642
  result = await client.get_prompt("test_prompt", {"name": "World"})
643
+ assert len(result.messages) == 1
644
+ message = result.messages[0]
645
  assert isinstance(message.content, TextContent)
646
  assert message.content.text == "Hello, World!"
647
 
648
  result = await client.get_prompt(
649
  "test_prompt", {"name": "World", "greeting": "Hi"}
650
  )
651
+ assert len(result.messages) == 1
652
+ message = result.messages[0]
653
  assert isinstance(message.content, TextContent)
654
  assert message.content.text == "Hi, World!"
655
 
 
668
 
669
  async with Client(mcp) as client:
670
  result = await client.get_prompt("test_prompt")
671
+ assert len(result.messages) == 1
672
+ message = result.messages[0]
673
  assert isinstance(message.content, TextContent)
674
  assert message.content.text == "My prefix: Hello, world!"
675
 
 
687
 
688
  async with Client(mcp) as client:
689
  result = await client.get_prompt("test_prompt")
690
+ assert len(result.messages) == 1
691
+ message = result.messages[0]
692
  assert isinstance(message.content, TextContent)
693
  assert message.content.text == "Class prefix: Hello, world!"
694
 
 
703
 
704
  async with Client(mcp) as client:
705
  result = await client.get_prompt("test_prompt")
706
+ assert len(result.messages) == 1
707
+ message = result.messages[0]
708
  assert isinstance(message.content, TextContent)
709
  assert message.content.text == "Static Hello, world!"
710
 
 
717
 
718
  async with Client(mcp) as client:
719
  result = await client.get_prompt("test_prompt")
720
+ assert len(result.messages) == 1
721
+ message = result.messages[0]
722
  assert isinstance(message.content, TextContent)
723
  assert message.content.text == "Async Hello, world!"
724
 
tests/server/test_server_interactions.py CHANGED
@@ -18,7 +18,7 @@ from pydantic import AnyUrl, Field
18
 
19
  from fastmcp import Client, Context, FastMCP
20
  from fastmcp.exceptions import ClientError
21
- from fastmcp.prompts.prompt import EmbeddedResource, Message, UserMessage
22
  from fastmcp.resources import FileResource, FunctionResource
23
  from fastmcp.utilities.types import Image
24
 
@@ -1267,8 +1267,8 @@ class TestPrompts:
1267
 
1268
  async with Client(mcp) as client:
1269
  result = await client.get_prompt("fn", {"name": "World"})
1270
- assert len(result) == 1
1271
- message = result[0]
1272
  assert message.role == "user"
1273
  content = message.content
1274
  assert isinstance(content, TextContent)
@@ -1279,8 +1279,9 @@ class TestPrompts:
1279
  mcp = FastMCP()
1280
 
1281
  @mcp.prompt()
1282
- def fn() -> Message:
1283
- return UserMessage(
 
1284
  content=EmbeddedResource(
1285
  type="resource",
1286
  resource=TextResourceContents(
@@ -1288,13 +1289,13 @@ class TestPrompts:
1288
  text="File contents",
1289
  mimeType="text/plain",
1290
  ),
1291
- )
1292
  )
1293
 
1294
  async with Client(mcp) as client:
1295
  result = await client.get_prompt("fn")
1296
- assert result[0].role == "user"
1297
- content = result[0].content
1298
  assert isinstance(content, EmbeddedResource)
1299
  resource = content.resource
1300
  assert isinstance(resource, TextResourceContents)
@@ -1370,6 +1371,6 @@ class TestPromptContext:
1370
 
1371
  async with Client(mcp) as client:
1372
  result = await client.get_prompt("prompt_fn", {"name": "World"})
1373
- assert len(result) == 1
1374
- message = result[0]
1375
  assert message.role == "user"
 
18
 
19
  from fastmcp import Client, Context, FastMCP
20
  from fastmcp.exceptions import ClientError
21
+ from fastmcp.prompts.prompt import EmbeddedResource, PromptMessage
22
  from fastmcp.resources import FileResource, FunctionResource
23
  from fastmcp.utilities.types import Image
24
 
 
1267
 
1268
  async with Client(mcp) as client:
1269
  result = await client.get_prompt("fn", {"name": "World"})
1270
+ assert len(result.messages) == 1
1271
+ message = result.messages[0]
1272
  assert message.role == "user"
1273
  content = message.content
1274
  assert isinstance(content, TextContent)
 
1279
  mcp = FastMCP()
1280
 
1281
  @mcp.prompt()
1282
+ def fn() -> PromptMessage:
1283
+ return PromptMessage(
1284
+ role="user",
1285
  content=EmbeddedResource(
1286
  type="resource",
1287
  resource=TextResourceContents(
 
1289
  text="File contents",
1290
  mimeType="text/plain",
1291
  ),
1292
+ ),
1293
  )
1294
 
1295
  async with Client(mcp) as client:
1296
  result = await client.get_prompt("fn")
1297
+ assert result.messages[0].role == "user"
1298
+ content = result.messages[0].content
1299
  assert isinstance(content, EmbeddedResource)
1300
  resource = content.resource
1301
  assert isinstance(resource, TextResourceContents)
 
1371
 
1372
  async with Client(mcp) as client:
1373
  result = await client.get_prompt("prompt_fn", {"name": "World"})
1374
+ assert len(result.messages) == 1
1375
+ message = result.messages[0]
1376
  assert message.role == "user"