fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/prompts/prompt.py CHANGED
@@ -2,51 +2,94 @@
2
2
 
3
3
  from __future__ import annotations as _annotations
4
4
 
5
- import inspect
6
- import json
7
- from collections.abc import Awaitable, Callable, Sequence
8
- from typing import Annotated, Any
5
+ import warnings
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
9
8
 
9
+ import pydantic
10
10
  import pydantic_core
11
- from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
11
+
12
+ if TYPE_CHECKING:
13
+ from docket import Docket
14
+ from docket.execution import Execution
15
+
16
+ from fastmcp.prompts.function_prompt import FunctionPrompt
17
+ import mcp.types
18
+ from mcp import GetPromptResult
19
+ from mcp.types import (
20
+ EmbeddedResource,
21
+ Icon,
22
+ PromptMessage,
23
+ TextContent,
24
+ )
12
25
  from mcp.types import Prompt as SDKPrompt
13
26
  from mcp.types import PromptArgument as SDKPromptArgument
14
- from pydantic import Field, TypeAdapter
27
+ from pydantic import Field
15
28
 
16
- from fastmcp.exceptions import PromptError
17
- from fastmcp.server.dependencies import get_context, without_injected_parameters
18
- from fastmcp.server.tasks.config import TaskConfig
29
+ from fastmcp.server.tasks.config import TaskConfig, TaskMeta
30
+ from fastmcp.tools.tool import AuthCheckCallable
19
31
  from fastmcp.utilities.components import FastMCPComponent
20
- from fastmcp.utilities.json_schema import compress_schema
21
32
  from fastmcp.utilities.logging import get_logger
22
33
  from fastmcp.utilities.types import (
23
34
  FastMCPBaseModel,
24
- get_cached_typeadapter,
25
35
  )
26
36
 
27
37
  logger = get_logger(__name__)
28
38
 
29
39
 
30
- def Message(
31
- content: str | ContentBlock, role: Role | None = None, **kwargs: Any
32
- ) -> PromptMessage:
33
- """A user-friendly constructor for PromptMessage."""
34
- if isinstance(content, str):
35
- content = TextContent(type="text", text=content)
36
- if role is None:
37
- role = "user"
38
- return PromptMessage(content=content, role=role, **kwargs)
40
+ class Message(pydantic.BaseModel):
41
+ """Wrapper for prompt message with auto-serialization.
39
42
 
43
+ Accepts any content - strings pass through, other types
44
+ (dict, list, BaseModel) are JSON-serialized to text.
40
45
 
41
- message_validator = TypeAdapter[PromptMessage](PromptMessage)
46
+ Example:
47
+ ```python
48
+ from fastmcp.prompts import Message
42
49
 
43
- SyncPromptResult = (
44
- str
45
- | PromptMessage
46
- | dict[str, Any]
47
- | Sequence[str | PromptMessage | dict[str, Any]]
48
- )
49
- PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
50
+ # String content (user role by default)
51
+ Message("Hello, world!")
52
+
53
+ # Explicit role
54
+ Message("I can help with that.", role="assistant")
55
+
56
+ # Auto-serialized to JSON
57
+ Message({"key": "value"})
58
+ Message(["item1", "item2"])
59
+ ```
60
+ """
61
+
62
+ role: Literal["user", "assistant"]
63
+ content: TextContent | EmbeddedResource
64
+
65
+ def __init__(
66
+ self,
67
+ content: Any,
68
+ role: Literal["user", "assistant"] = "user",
69
+ ):
70
+ """Create Message with automatic serialization.
71
+
72
+ Args:
73
+ content: The message content. str passes through directly.
74
+ TextContent and EmbeddedResource pass through.
75
+ Other types (dict, list, BaseModel) are JSON-serialized.
76
+ role: The message role, either "user" or "assistant".
77
+ """
78
+ # Handle already-wrapped content types
79
+ if isinstance(content, (TextContent, EmbeddedResource)):
80
+ normalized_content: TextContent | EmbeddedResource = content
81
+ elif isinstance(content, str):
82
+ normalized_content = TextContent(type="text", text=content)
83
+ else:
84
+ # dict, list, BaseModel → JSON string
85
+ serialized = pydantic_core.to_json(content, fallback=str).decode()
86
+ normalized_content = TextContent(type="text", text=serialized)
87
+
88
+ super().__init__(role=role, content=normalized_content)
89
+
90
+ def to_mcp_prompt_message(self) -> PromptMessage:
91
+ """Convert to MCP PromptMessage."""
92
+ return PromptMessage(role=self.role, content=self.content)
50
93
 
51
94
 
52
95
  class PromptArgument(FastMCPBaseModel):
@@ -61,33 +104,102 @@ class PromptArgument(FastMCPBaseModel):
61
104
  )
62
105
 
63
106
 
107
+ class PromptResult(pydantic.BaseModel):
108
+ """Canonical result type for prompt rendering.
109
+
110
+ Provides explicit control over prompt responses: multiple messages,
111
+ roles, and metadata at both the message and result level.
112
+
113
+ Accepts:
114
+ - str: Wrapped as single Message (user role)
115
+ - list[Message]: Used directly for multiple messages or custom roles
116
+
117
+ Example:
118
+ ```python
119
+ from fastmcp import FastMCP
120
+ from fastmcp.prompts import PromptResult, Message
121
+
122
+ mcp = FastMCP()
123
+
124
+ # Simple string content
125
+ @mcp.prompt
126
+ def greet() -> PromptResult:
127
+ return PromptResult("Hello!")
128
+
129
+ # Multiple messages with roles
130
+ @mcp.prompt
131
+ def conversation() -> PromptResult:
132
+ return PromptResult([
133
+ Message("What's the weather?"),
134
+ Message("It's sunny today.", role="assistant"),
135
+ ])
136
+ ```
137
+ """
138
+
139
+ messages: list[Message]
140
+ description: str | None = None
141
+ meta: dict[str, Any] | None = None
142
+
143
+ def __init__(
144
+ self,
145
+ messages: str | list[Message],
146
+ description: str | None = None,
147
+ meta: dict[str, Any] | None = None,
148
+ ):
149
+ """Create PromptResult.
150
+
151
+ Args:
152
+ messages: String or list of Message objects.
153
+ description: Optional description of the prompt result.
154
+ meta: Optional metadata about the prompt result.
155
+ """
156
+ normalized = self._normalize_messages(messages)
157
+ super().__init__(messages=normalized, description=description, meta=meta)
158
+
159
+ @staticmethod
160
+ def _normalize_messages(
161
+ messages: str | list[Message],
162
+ ) -> list[Message]:
163
+ """Normalize input to list[Message]."""
164
+ if isinstance(messages, str):
165
+ return [Message(messages)]
166
+ if isinstance(messages, list):
167
+ # Validate all items are Message
168
+ for i, item in enumerate(messages):
169
+ if not isinstance(item, Message):
170
+ raise TypeError(
171
+ f"messages[{i}] must be Message, got {type(item).__name__}. "
172
+ f"Use Message({item!r}) to wrap the value."
173
+ )
174
+ return messages
175
+ raise TypeError(
176
+ f"messages must be str or list[Message], got {type(messages).__name__}"
177
+ )
178
+
179
+ def to_mcp_prompt_result(self) -> GetPromptResult:
180
+ """Convert to MCP GetPromptResult."""
181
+ mcp_messages = [m.to_mcp_prompt_message() for m in self.messages]
182
+ return GetPromptResult(
183
+ description=self.description,
184
+ messages=mcp_messages,
185
+ _meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
186
+ )
187
+
188
+
64
189
  class Prompt(FastMCPComponent):
65
190
  """A prompt template that can be rendered with parameters."""
66
191
 
192
+ KEY_PREFIX: ClassVar[str] = "prompt"
193
+
67
194
  arguments: list[PromptArgument] | None = Field(
68
195
  default=None, description="Arguments that can be passed to the prompt"
69
196
  )
70
-
71
- def enable(self) -> None:
72
- super().enable()
73
- try:
74
- context = get_context()
75
- context._queue_prompt_list_changed() # type: ignore[private-use]
76
- except RuntimeError:
77
- pass # No context available
78
-
79
- def disable(self) -> None:
80
- super().disable()
81
- try:
82
- context = get_context()
83
- context._queue_prompt_list_changed() # type: ignore[private-use]
84
- except RuntimeError:
85
- pass # No context available
197
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = Field(
198
+ default=None, description="Authorization checks for this prompt", exclude=True
199
+ )
86
200
 
87
201
  def to_mcp_prompt(
88
202
  self,
89
- *,
90
- include_fastmcp_meta: bool | None = None,
91
203
  **overrides: Any,
92
204
  ) -> SDKPrompt:
93
205
  """Convert the prompt to an MCP prompt."""
@@ -106,276 +218,214 @@ class Prompt(FastMCPComponent):
106
218
  arguments=arguments,
107
219
  title=overrides.get("title", self.title),
108
220
  icons=overrides.get("icons", self.icons),
109
- _meta=overrides.get(
110
- "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
221
+ _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
222
+ "_meta", self.get_meta()
111
223
  ),
112
224
  )
113
225
 
114
- @staticmethod
226
+ @classmethod
115
227
  def from_function(
116
- fn: Callable[..., PromptResult | Awaitable[PromptResult]],
228
+ cls,
229
+ fn: Callable[..., Any],
230
+ *,
117
231
  name: str | None = None,
232
+ version: str | int | None = None,
118
233
  title: str | None = None,
119
234
  description: str | None = None,
120
235
  icons: list[Icon] | None = None,
121
236
  tags: set[str] | None = None,
122
- enabled: bool | None = None,
123
237
  meta: dict[str, Any] | None = None,
124
238
  task: bool | TaskConfig | None = None,
239
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
125
240
  ) -> FunctionPrompt:
126
241
  """Create a Prompt from a function.
127
242
 
128
243
  The function can return:
129
- - A string (converted to a message)
130
- - A Message object
131
- - A dict (converted to a message)
132
- - A sequence of any of the above
244
+ - str: wrapped as single user Message
245
+ - list[Message | str]: converted to list[Message]
246
+ - PromptResult: used directly
133
247
  """
248
+ from fastmcp.prompts.function_prompt import FunctionPrompt
249
+
134
250
  return FunctionPrompt.from_function(
135
251
  fn=fn,
136
252
  name=name,
253
+ version=version,
137
254
  title=title,
138
255
  description=description,
139
256
  icons=icons,
140
257
  tags=tags,
141
- enabled=enabled,
142
258
  meta=meta,
143
259
  task=task,
260
+ auth=auth,
144
261
  )
145
262
 
146
263
  async def render(
147
264
  self,
148
265
  arguments: dict[str, Any] | None = None,
149
- ) -> list[PromptMessage]:
266
+ ) -> str | list[Message | str] | PromptResult:
150
267
  """Render the prompt with arguments.
151
268
 
152
- This method is not implemented in the base Prompt class and must be
153
- implemented by subclasses.
269
+ Subclasses must implement this method. Return one of:
270
+ - str: Wrapped as single user Message
271
+ - list[Message | str]: Converted to list[Message]
272
+ - PromptResult: Used directly
154
273
  """
155
274
  raise NotImplementedError("Subclasses must implement render()")
156
275
 
276
+ def convert_result(self, raw_value: Any) -> PromptResult:
277
+ """Convert a raw return value to PromptResult.
157
278
 
158
- class FunctionPrompt(Prompt):
159
- """A prompt that is a function."""
160
-
161
- fn: Callable[..., PromptResult | Awaitable[PromptResult]]
162
- task_config: Annotated[
163
- TaskConfig,
164
- Field(description="Background task execution configuration (SEP-1686)."),
165
- ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
279
+ Accepts:
280
+ - PromptResult: passed through
281
+ - str: wrapped as single Message
282
+ - list[Message | str]: converted to list[Message]
166
283
 
167
- @classmethod
168
- def from_function(
169
- cls,
170
- fn: Callable[..., PromptResult | Awaitable[PromptResult]],
171
- name: str | None = None,
172
- title: str | None = None,
173
- description: str | None = None,
174
- icons: list[Icon] | None = None,
175
- tags: set[str] | None = None,
176
- enabled: bool | None = None,
177
- meta: dict[str, Any] | None = None,
178
- task: bool | TaskConfig | None = None,
179
- ) -> FunctionPrompt:
180
- """Create a Prompt from a function.
181
-
182
- The function can return:
183
- - A string (converted to a message)
184
- - A Message object
185
- - A dict (converted to a message)
186
- - A sequence of any of the above
284
+ Raises:
285
+ TypeError: for unsupported types
187
286
  """
188
-
189
- func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
190
-
191
- if func_name == "<lambda>":
192
- raise ValueError("You must provide a name for lambda functions")
193
- # Reject functions with *args or **kwargs
194
- sig = inspect.signature(fn)
195
- for param in sig.parameters.values():
196
- if param.kind == inspect.Parameter.VAR_POSITIONAL:
197
- raise ValueError("Functions with *args are not supported as prompts")
198
- if param.kind == inspect.Parameter.VAR_KEYWORD:
199
- raise ValueError("Functions with **kwargs are not supported as prompts")
200
-
201
- description = description or inspect.getdoc(fn)
202
-
203
- # Normalize task to TaskConfig and validate
204
- if task is None:
205
- task_config = TaskConfig(mode="forbidden")
206
- elif isinstance(task, bool):
207
- task_config = TaskConfig.from_bool(task)
208
- else:
209
- task_config = task
210
- task_config.validate_function(fn, func_name)
211
-
212
- # if the fn is a callable class, we need to get the __call__ method from here out
213
- if not inspect.isroutine(fn):
214
- fn = fn.__call__
215
- # if the fn is a staticmethod, we need to work with the underlying function
216
- if isinstance(fn, staticmethod):
217
- fn = fn.__func__ # type: ignore[assignment]
218
-
219
- # Wrap fn to handle dependency resolution internally
220
- wrapped_fn = without_injected_parameters(fn)
221
- type_adapter = get_cached_typeadapter(wrapped_fn)
222
- parameters = type_adapter.json_schema()
223
- parameters = compress_schema(parameters, prune_titles=True)
224
-
225
- # Convert parameters to PromptArguments
226
- arguments: list[PromptArgument] = []
227
- if "properties" in parameters:
228
- for param_name, param in parameters["properties"].items():
229
- arg_description = param.get("description")
230
-
231
- # For non-string parameters, append JSON schema info to help users
232
- # understand the expected format when passing as strings (MCP requirement)
233
- if param_name in sig.parameters:
234
- sig_param = sig.parameters[param_name]
235
- if (
236
- sig_param.annotation != inspect.Parameter.empty
237
- and sig_param.annotation is not str
238
- ):
239
- # Get the JSON schema for this specific parameter type
240
- try:
241
- param_adapter = get_cached_typeadapter(sig_param.annotation)
242
- param_schema = param_adapter.json_schema()
243
-
244
- # Create compact schema representation
245
- schema_str = json.dumps(param_schema, separators=(",", ":"))
246
-
247
- # Append schema info to description
248
- schema_note = f"Provide as a JSON string matching the following schema: {schema_str}"
249
- if arg_description:
250
- arg_description = f"{arg_description}\n\n{schema_note}"
251
- else:
252
- arg_description = schema_note
253
- except Exception:
254
- # If schema generation fails, skip enhancement
255
- pass
256
-
257
- arguments.append(
258
- PromptArgument(
259
- name=param_name,
260
- description=arg_description,
261
- required=param_name in parameters.get("required", []),
287
+ if isinstance(raw_value, PromptResult):
288
+ return raw_value
289
+
290
+ if isinstance(raw_value, str):
291
+ return PromptResult(raw_value, description=self.description, meta=self.meta)
292
+
293
+ if isinstance(raw_value, list | tuple):
294
+ messages: list[Message] = []
295
+ for i, item in enumerate(raw_value):
296
+ if isinstance(item, Message):
297
+ messages.append(item)
298
+ elif isinstance(item, str):
299
+ messages.append(Message(item))
300
+ else:
301
+ raise TypeError(
302
+ f"messages[{i}] must be Message or str, got {type(item).__name__}. "
303
+ f"Use Message({item!r}) to wrap the value."
262
304
  )
263
- )
305
+ return PromptResult(messages, description=self.description, meta=self.meta)
264
306
 
265
- return cls(
266
- name=func_name,
267
- title=title,
268
- description=description,
269
- icons=icons,
270
- arguments=arguments,
271
- tags=tags or set(),
272
- enabled=enabled if enabled is not None else True,
273
- fn=wrapped_fn,
274
- meta=meta,
275
- task_config=task_config,
307
+ raise TypeError(
308
+ f"Prompt must return str, list[Message], or PromptResult, "
309
+ f"got {type(raw_value).__name__}"
276
310
  )
277
311
 
278
- def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
279
- """Convert string arguments to expected types based on function signature."""
280
- from fastmcp.server.dependencies import without_injected_parameters
312
+ @overload
313
+ async def _render(
314
+ self,
315
+ arguments: dict[str, Any] | None = None,
316
+ task_meta: None = None,
317
+ ) -> PromptResult: ...
281
318
 
282
- wrapper_fn = without_injected_parameters(self.fn)
283
- sig = inspect.signature(wrapper_fn)
284
- converted_kwargs = {}
319
+ @overload
320
+ async def _render(
321
+ self,
322
+ arguments: dict[str, Any] | None,
323
+ task_meta: TaskMeta,
324
+ ) -> mcp.types.CreateTaskResult: ...
285
325
 
286
- for param_name, param_value in kwargs.items():
287
- if param_name in sig.parameters:
288
- param = sig.parameters[param_name]
326
+ async def _render(
327
+ self,
328
+ arguments: dict[str, Any] | None = None,
329
+ task_meta: TaskMeta | None = None,
330
+ ) -> PromptResult | mcp.types.CreateTaskResult:
331
+ """Server entry point that handles task routing.
332
+
333
+ This allows ANY Prompt subclass to support background execution by setting
334
+ task_config.mode to "supported" or "required". The server calls this
335
+ method instead of render() directly.
336
+
337
+ Args:
338
+ arguments: Prompt arguments
339
+ task_meta: If provided, execute as background task and return
340
+ CreateTaskResult. If None (default), execute synchronously and
341
+ return PromptResult.
342
+
343
+ Returns:
344
+ PromptResult when task_meta is None.
345
+ CreateTaskResult when task_meta is provided.
346
+
347
+ Subclasses can override this to customize task routing behavior.
348
+ For example, FastMCPProviderPrompt overrides to delegate to child
349
+ middleware without submitting to Docket.
350
+ """
351
+ from fastmcp.server.tasks.routing import check_background_task
289
352
 
290
- # If parameter has no annotation or annotation is str, pass as-is
291
- if (
292
- param.annotation == inspect.Parameter.empty
293
- or param.annotation is str
294
- ) or not isinstance(param_value, str):
295
- converted_kwargs[param_name] = param_value
296
- else:
297
- # Try to convert string argument using type adapter
298
- try:
299
- adapter = get_cached_typeadapter(param.annotation)
300
- # Try JSON parsing first for complex types
301
- try:
302
- converted_kwargs[param_name] = adapter.validate_json(
303
- param_value
304
- )
305
- except (ValueError, TypeError, pydantic_core.ValidationError):
306
- # Fallback to direct validation
307
- converted_kwargs[param_name] = adapter.validate_python(
308
- param_value
309
- )
310
- except (ValueError, TypeError, pydantic_core.ValidationError) as e:
311
- # If conversion fails, provide informative error
312
- raise PromptError(
313
- f"Could not convert argument '{param_name}' with value '{param_value}' "
314
- f"to expected type {param.annotation}. Error: {e}"
315
- ) from e
316
- else:
317
- # Parameter not in function signature, pass as-is
318
- converted_kwargs[param_name] = param_value
319
-
320
- return converted_kwargs
353
+ task_result = await check_background_task(
354
+ component=self,
355
+ task_type="prompt",
356
+ arguments=arguments,
357
+ task_meta=task_meta,
358
+ )
359
+ if task_result:
360
+ return task_result
321
361
 
322
- async def render(
362
+ # Synchronous execution
363
+ result = await self.render(arguments)
364
+ return self.convert_result(result)
365
+
366
+ def register_with_docket(self, docket: Docket) -> None:
367
+ """Register this prompt with docket for background execution."""
368
+ if not self.task_config.supports_tasks():
369
+ return
370
+ docket.register(self.render, names=[self.key])
371
+
372
+ async def add_to_docket( # type: ignore[override]
323
373
  self,
324
- arguments: dict[str, Any] | None = None,
325
- ) -> list[PromptMessage]:
326
- """Render the prompt with arguments."""
327
- # Validate required arguments
328
- if self.arguments:
329
- required = {arg.name for arg in self.arguments if arg.required}
330
- provided = set(arguments or {})
331
- missing = required - provided
332
- if missing:
333
- raise ValueError(f"Missing required arguments: {missing}")
334
-
335
- try:
336
- # Prepare arguments
337
- kwargs = arguments.copy() if arguments else {}
338
-
339
- # Convert string arguments to expected types BEFORE validation
340
- kwargs = self._convert_string_arguments(kwargs)
341
-
342
- # self.fn is wrapped by without_injected_parameters which handles
343
- # dependency resolution internally
344
- result = self.fn(**kwargs)
345
- if inspect.isawaitable(result):
346
- result = await result
347
-
348
- # Validate messages
349
- if not isinstance(result, list | tuple):
350
- result = [result]
351
-
352
- # Convert result to messages
353
- messages: list[PromptMessage] = []
354
- for msg in result:
355
- try:
356
- if isinstance(msg, PromptMessage):
357
- messages.append(msg)
358
- elif isinstance(msg, str):
359
- messages.append(
360
- PromptMessage(
361
- role="user",
362
- content=TextContent(type="text", text=msg),
363
- )
364
- )
365
- else:
366
- content = pydantic_core.to_json(msg, fallback=str).decode()
367
- messages.append(
368
- PromptMessage(
369
- role="user",
370
- content=TextContent(type="text", text=content),
371
- )
372
- )
373
- except Exception as e:
374
- raise PromptError(
375
- "Could not convert prompt result to message."
376
- ) from e
374
+ docket: Docket,
375
+ arguments: dict[str, Any] | None,
376
+ *,
377
+ fn_key: str | None = None,
378
+ task_key: str | None = None,
379
+ **kwargs: Any,
380
+ ) -> Execution:
381
+ """Schedule this prompt for background execution via docket.
382
+
383
+ Args:
384
+ docket: The Docket instance
385
+ arguments: Prompt arguments
386
+ fn_key: Function lookup key in Docket registry (defaults to self.key)
387
+ task_key: Redis storage key for the result
388
+ **kwargs: Additional kwargs passed to docket.add()
389
+ """
390
+ lookup_key = fn_key or self.key
391
+ if task_key:
392
+ kwargs["key"] = task_key
393
+ return await docket.add(lookup_key, **kwargs)(arguments)
394
+
395
+ def get_span_attributes(self) -> dict[str, Any]:
396
+ return super().get_span_attributes() | {
397
+ "fastmcp.component.type": "prompt",
398
+ "fastmcp.provider.type": "LocalProvider",
399
+ }
400
+
401
+
402
+ __all__ = [
403
+ "Message",
404
+ "Prompt",
405
+ "PromptArgument",
406
+ "PromptResult",
407
+ ]
408
+
409
+
410
+ def __getattr__(name: str) -> Any:
411
+ """Deprecated re-exports for backwards compatibility."""
412
+ deprecated_exports = {
413
+ "FunctionPrompt": "FunctionPrompt",
414
+ "prompt": "prompt",
415
+ }
416
+
417
+ if name in deprecated_exports:
418
+ import fastmcp
419
+
420
+ if fastmcp.settings.deprecation_warnings:
421
+ warnings.warn(
422
+ f"Importing {name} from fastmcp.prompts.prompt is deprecated. "
423
+ f"Import from fastmcp.prompts.function_prompt instead.",
424
+ DeprecationWarning,
425
+ stacklevel=2,
426
+ )
427
+ from fastmcp.prompts import function_prompt
377
428
 
378
- return messages
379
- except Exception as e:
380
- logger.exception(f"Error rendering prompt {self.name}")
381
- raise PromptError(f"Error rendering prompt {self.name}.") from e
429
+ return getattr(function_prompt, name)
430
+
431
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")