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/tools/tool.py CHANGED
@@ -1,16 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import inspect
4
3
  import warnings
5
4
  from collections.abc import Callable
6
- from dataclasses import dataclass
7
5
  from typing import (
8
6
  TYPE_CHECKING,
9
7
  Annotated,
10
8
  Any,
11
- Generic,
9
+ ClassVar,
12
10
  TypeAlias,
13
- get_type_hints,
11
+ overload,
14
12
  )
15
13
 
16
14
  import mcp.types
@@ -25,14 +23,10 @@ from mcp.types import (
25
23
  ToolExecution,
26
24
  )
27
25
  from mcp.types import Tool as MCPTool
28
- from pydantic import Field, PydanticSchemaGenerationError, model_validator
29
- from typing_extensions import TypeVar
26
+ from pydantic import BaseModel, Field, model_validator
30
27
 
31
- import fastmcp
32
- from fastmcp.server.dependencies import get_context, without_injected_parameters
33
- from fastmcp.server.tasks.config import TaskConfig
28
+ from fastmcp.server.tasks.config import TaskConfig, TaskMeta
34
29
  from fastmcp.utilities.components import FastMCPComponent
35
- from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref
36
30
  from fastmcp.utilities.logging import get_logger
37
31
  from fastmcp.utilities.types import (
38
32
  Audio,
@@ -40,28 +34,22 @@ from fastmcp.utilities.types import (
40
34
  Image,
41
35
  NotSet,
42
36
  NotSetT,
43
- create_function_without_params,
44
- get_cached_typeadapter,
45
- replace_type,
46
37
  )
47
38
 
48
- if TYPE_CHECKING:
49
- from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
50
-
51
- logger = get_logger(__name__)
52
-
53
- T = TypeVar("T", default=Any)
39
+ # Runtime type alias for auth checks to avoid circular imports with authorization.py
40
+ # AuthCheck is Callable[[AuthContext], bool] but we use Any to avoid the import
41
+ AuthCheckCallable: TypeAlias = Callable[[Any], bool]
54
42
 
43
+ if TYPE_CHECKING:
44
+ from docket import Docket
45
+ from docket.execution import Execution
55
46
 
56
- @dataclass
57
- class _WrappedResult(Generic[T]):
58
- """Generic wrapper for non-object return types."""
59
-
60
- result: T
47
+ from fastmcp.tools.function_tool import FunctionTool
48
+ from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
61
49
 
50
+ # Re-export from function_tool module
62
51
 
63
- class _UnserializableType:
64
- pass
52
+ logger = get_logger(__name__)
65
53
 
66
54
 
67
55
  ToolResultSerializerType: TypeAlias = Callable[[Any], str]
@@ -71,7 +59,17 @@ def default_serializer(data: Any) -> str:
71
59
  return pydantic_core.to_json(data, fallback=str).decode()
72
60
 
73
61
 
74
- class ToolResult:
62
+ class ToolResult(BaseModel):
63
+ content: list[ContentBlock] = Field(
64
+ description="List of content blocks for the tool result"
65
+ )
66
+ structured_content: dict[str, Any] | None = Field(
67
+ default=None, description="Structured content matching the tool's output schema"
68
+ )
69
+ meta: dict[str, Any] | None = Field(
70
+ default=None, description="Runtime metadata about the tool execution"
71
+ )
72
+
75
73
  def __init__(
76
74
  self,
77
75
  content: list[ContentBlock] | Any | None = None,
@@ -83,8 +81,7 @@ class ToolResult:
83
81
  elif content is None:
84
82
  content = structured_content
85
83
 
86
- self.content: list[ContentBlock] = _convert_to_content(result=content)
87
- self.meta: dict[str, Any] | None = meta
84
+ converted_content: list[ContentBlock] = _convert_to_content(result=content)
88
85
 
89
86
  if structured_content is not None:
90
87
  try:
@@ -102,7 +99,10 @@ class ToolResult:
102
99
  f"Got {type(structured_content).__name__}: {structured_content!r}. "
103
100
  "Tools should wrap non-dict values based on their output_schema."
104
101
  )
105
- self.structured_content: dict[str, Any] | None = structured_content
102
+
103
+ super().__init__(
104
+ content=converted_content, structured_content=structured_content, meta=meta
105
+ )
106
106
 
107
107
  def to_mcp_result(
108
108
  self,
@@ -113,7 +113,7 @@ class ToolResult:
113
113
  return CallToolResult(
114
114
  structuredContent=self.structured_content,
115
115
  content=self.content,
116
- _meta=self.meta,
116
+ _meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
117
117
  )
118
118
  if self.structured_content is None:
119
119
  return self.content
@@ -123,6 +123,8 @@ class ToolResult:
123
123
  class Tool(FastMCPComponent):
124
124
  """Internal tool registration info."""
125
125
 
126
+ KEY_PREFIX: ClassVar[str] = "tool"
127
+
126
128
  parameters: Annotated[
127
129
  dict[str, Any], Field(description="JSON schema for tool parameters")
128
130
  ]
@@ -139,7 +141,19 @@ class Tool(FastMCPComponent):
139
141
  ] = None
140
142
  serializer: Annotated[
141
143
  ToolResultSerializerType | None,
142
- Field(description="Optional custom serializer for tool results"),
144
+ Field(
145
+ description="Deprecated. Return ToolResult from your tools for full control over serialization."
146
+ ),
147
+ ] = None
148
+ auth: Annotated[
149
+ AuthCheckCallable | list[AuthCheckCallable] | None,
150
+ Field(description="Authorization checks for this tool", exclude=True),
151
+ ] = None
152
+ timeout: Annotated[
153
+ float | None,
154
+ Field(
155
+ description="Execution timeout in seconds. If None, no timeout is applied."
156
+ ),
143
157
  ] = None
144
158
 
145
159
  @model_validator(mode="after")
@@ -148,26 +162,8 @@ class Tool(FastMCPComponent):
148
162
  validate_and_warn_tool_name(self.name)
149
163
  return self
150
164
 
151
- def enable(self) -> None:
152
- super().enable()
153
- try:
154
- context = get_context()
155
- context._queue_tool_list_changed() # type: ignore[private-use]
156
- except RuntimeError:
157
- pass # No context available
158
-
159
- def disable(self) -> None:
160
- super().disable()
161
- try:
162
- context = get_context()
163
- context._queue_tool_list_changed() # type: ignore[private-use]
164
- except RuntimeError:
165
- pass # No context available
166
-
167
165
  def to_mcp_tool(
168
166
  self,
169
- *,
170
- include_fastmcp_meta: bool | None = None,
171
167
  **overrides: Any,
172
168
  ) -> MCPTool:
173
169
  """Convert the FastMCP tool to an MCP tool."""
@@ -187,15 +183,18 @@ class Tool(FastMCPComponent):
187
183
  icons=overrides.get("icons", self.icons),
188
184
  annotations=overrides.get("annotations", self.annotations),
189
185
  execution=overrides.get("execution", self.execution),
190
- _meta=overrides.get(
191
- "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
186
+ _meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
187
+ "_meta", self.get_meta()
192
188
  ),
193
189
  )
194
190
 
195
- @staticmethod
191
+ @classmethod
196
192
  def from_function(
193
+ cls,
197
194
  fn: Callable[..., Any],
195
+ *,
198
196
  name: str | None = None,
197
+ version: str | int | None = None,
199
198
  title: str | None = None,
200
199
  description: str | None = None,
201
200
  icons: list[Icon] | None = None,
@@ -203,15 +202,19 @@ class Tool(FastMCPComponent):
203
202
  annotations: ToolAnnotations | None = None,
204
203
  exclude_args: list[str] | None = None,
205
204
  output_schema: dict[str, Any] | NotSetT | None = NotSet,
206
- serializer: ToolResultSerializerType | None = None,
205
+ serializer: ToolResultSerializerType | None = None, # Deprecated
207
206
  meta: dict[str, Any] | None = None,
208
- enabled: bool | None = None,
209
207
  task: bool | TaskConfig | None = None,
208
+ timeout: float | None = None,
209
+ auth: AuthCheckCallable | list[AuthCheckCallable] | None = None,
210
210
  ) -> FunctionTool:
211
211
  """Create a Tool from a function."""
212
+ from fastmcp.tools.function_tool import FunctionTool
213
+
212
214
  return FunctionTool.from_function(
213
215
  fn=fn,
214
216
  name=name,
217
+ version=version,
215
218
  title=title,
216
219
  description=description,
217
220
  icons=icons,
@@ -221,8 +224,9 @@ class Tool(FastMCPComponent):
221
224
  output_schema=output_schema,
222
225
  serializer=serializer,
223
226
  meta=meta,
224
- enabled=enabled,
225
227
  task=task,
228
+ timeout=timeout,
229
+ auth=auth,
226
230
  )
227
231
 
228
232
  async def run(self, arguments: dict[str, Any]) -> ToolResult:
@@ -237,6 +241,127 @@ class Tool(FastMCPComponent):
237
241
  """
238
242
  raise NotImplementedError("Subclasses must implement run()")
239
243
 
244
+ def convert_result(self, raw_value: Any) -> ToolResult:
245
+ """Convert a raw result to ToolResult.
246
+
247
+ Handles ToolResult passthrough and converts raw values using the tool's
248
+ attributes (serializer, output_schema) for proper conversion.
249
+ """
250
+ if isinstance(raw_value, ToolResult):
251
+ return raw_value
252
+
253
+ content = _convert_to_content(raw_value, serializer=self.serializer)
254
+
255
+ # Skip structured content for ContentBlock types only if no output_schema
256
+ # (if output_schema exists, MCP SDK requires structured_content)
257
+ if self.output_schema is None and (
258
+ isinstance(raw_value, ContentBlock | Audio | Image | File)
259
+ or (
260
+ isinstance(raw_value, list | tuple)
261
+ and any(isinstance(item, ContentBlock) for item in raw_value)
262
+ )
263
+ ):
264
+ return ToolResult(content=content)
265
+
266
+ try:
267
+ structured = pydantic_core.to_jsonable_python(raw_value)
268
+ except pydantic_core.PydanticSerializationError:
269
+ return ToolResult(content=content)
270
+
271
+ if self.output_schema is None:
272
+ # No schema - only use structured_content for dicts
273
+ if isinstance(structured, dict):
274
+ return ToolResult(content=content, structured_content=structured)
275
+ return ToolResult(content=content)
276
+
277
+ # Has output_schema - wrap if x-fastmcp-wrap-result is set
278
+ wrap_result = self.output_schema.get("x-fastmcp-wrap-result")
279
+ return ToolResult(
280
+ content=content,
281
+ structured_content={"result": structured} if wrap_result else structured,
282
+ )
283
+
284
+ @overload
285
+ async def _run(
286
+ self,
287
+ arguments: dict[str, Any],
288
+ task_meta: None = None,
289
+ ) -> ToolResult: ...
290
+
291
+ @overload
292
+ async def _run(
293
+ self,
294
+ arguments: dict[str, Any],
295
+ task_meta: TaskMeta,
296
+ ) -> mcp.types.CreateTaskResult: ...
297
+
298
+ async def _run(
299
+ self,
300
+ arguments: dict[str, Any],
301
+ task_meta: TaskMeta | None = None,
302
+ ) -> ToolResult | mcp.types.CreateTaskResult:
303
+ """Server entry point that handles task routing.
304
+
305
+ This allows ANY Tool subclass to support background execution by setting
306
+ task_config.mode to "supported" or "required". The server calls this
307
+ method instead of run() directly.
308
+
309
+ Args:
310
+ arguments: Tool arguments
311
+ task_meta: If provided, execute as background task and return
312
+ CreateTaskResult. If None (default), execute synchronously and
313
+ return ToolResult.
314
+
315
+ Returns:
316
+ ToolResult when task_meta is None.
317
+ CreateTaskResult when task_meta is provided.
318
+
319
+ Subclasses can override this to customize task routing behavior.
320
+ For example, FastMCPProviderTool overrides to delegate to child
321
+ middleware without submitting to Docket.
322
+ """
323
+ from fastmcp.server.tasks.routing import check_background_task
324
+
325
+ task_result = await check_background_task(
326
+ component=self,
327
+ task_type="tool",
328
+ arguments=arguments,
329
+ task_meta=task_meta,
330
+ )
331
+ if task_result:
332
+ return task_result
333
+
334
+ return await self.run(arguments)
335
+
336
+ def register_with_docket(self, docket: Docket) -> None:
337
+ """Register this tool with docket for background execution."""
338
+ if not self.task_config.supports_tasks():
339
+ return
340
+ docket.register(self.run, names=[self.key])
341
+
342
+ async def add_to_docket( # type: ignore[override]
343
+ self,
344
+ docket: Docket,
345
+ arguments: dict[str, Any],
346
+ *,
347
+ fn_key: str | None = None,
348
+ task_key: str | None = None,
349
+ **kwargs: Any,
350
+ ) -> Execution:
351
+ """Schedule this tool for background execution via docket.
352
+
353
+ Args:
354
+ docket: The Docket instance
355
+ arguments: Tool arguments
356
+ fn_key: Function lookup key in Docket registry (defaults to self.key)
357
+ task_key: Redis storage key for the result
358
+ **kwargs: Additional kwargs passed to docket.add()
359
+ """
360
+ lookup_key = fn_key or self.key
361
+ if task_key:
362
+ kwargs["key"] = task_key
363
+ return await docket.add(lookup_key, **kwargs)(arguments)
364
+
240
365
  @classmethod
241
366
  def from_tool(
242
367
  cls,
@@ -248,10 +373,9 @@ class Tool(FastMCPComponent):
248
373
  tags: set[str] | None = None,
249
374
  annotations: ToolAnnotations | NotSetT | None = NotSet,
250
375
  output_schema: dict[str, Any] | NotSetT | None = NotSet,
251
- serializer: ToolResultSerializerType | None = None,
376
+ serializer: ToolResultSerializerType | None = None, # Deprecated
252
377
  meta: dict[str, Any] | NotSetT | None = NotSet,
253
378
  transform_args: dict[str, ArgTransform] | None = None,
254
- enabled: bool | None = None,
255
379
  transform_fn: Callable[..., Any] | None = None,
256
380
  ) -> TransformedTool:
257
381
  from fastmcp.tools.tool_transform import TransformedTool
@@ -268,312 +392,13 @@ class Tool(FastMCPComponent):
268
392
  output_schema=output_schema,
269
393
  serializer=serializer,
270
394
  meta=meta,
271
- enabled=enabled,
272
395
  )
273
396
 
274
-
275
- class FunctionTool(Tool):
276
- fn: Callable[..., Any]
277
- task_config: Annotated[
278
- TaskConfig,
279
- Field(description="Background task execution configuration (SEP-1686)."),
280
- ] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
281
-
282
- def to_mcp_tool(
283
- self,
284
- *,
285
- include_fastmcp_meta: bool | None = None,
286
- **overrides: Any,
287
- ) -> MCPTool:
288
- """Convert the FastMCP tool to an MCP tool.
289
-
290
- Extends the base implementation to add task execution mode if enabled.
291
- """
292
- # Get base MCP tool from parent
293
- mcp_tool = super().to_mcp_tool(
294
- include_fastmcp_meta=include_fastmcp_meta, **overrides
295
- )
296
-
297
- # Add task execution mode per SEP-1686
298
- # Only set execution if not overridden and mode is not "forbidden"
299
- if self.task_config.mode != "forbidden" and "execution" not in overrides:
300
- mcp_tool.execution = ToolExecution(taskSupport=self.task_config.mode)
301
-
302
- return mcp_tool
303
-
304
- @classmethod
305
- def from_function(
306
- cls,
307
- fn: Callable[..., Any],
308
- name: str | None = None,
309
- title: str | None = None,
310
- description: str | None = None,
311
- icons: list[Icon] | None = None,
312
- tags: set[str] | None = None,
313
- annotations: ToolAnnotations | None = None,
314
- exclude_args: list[str] | None = None,
315
- output_schema: dict[str, Any] | NotSetT | None = NotSet,
316
- serializer: ToolResultSerializerType | None = None,
317
- meta: dict[str, Any] | None = None,
318
- enabled: bool | None = None,
319
- task: bool | TaskConfig | None = None,
320
- ) -> FunctionTool:
321
- """Create a Tool from a function."""
322
- if exclude_args and fastmcp.settings.deprecation_warnings:
323
- warnings.warn(
324
- "The `exclude_args` parameter is deprecated as of FastMCP 2.14. "
325
- "Use dependency injection with `Depends()` instead for better lifecycle management. "
326
- "See https://gofastmcp.com/servers/dependencies for examples.",
327
- DeprecationWarning,
328
- stacklevel=2,
329
- )
330
-
331
- parsed_fn = ParsedFunction.from_function(fn, exclude_args=exclude_args)
332
- func_name = name or parsed_fn.name
333
-
334
- if func_name == "<lambda>":
335
- raise ValueError("You must provide a name for lambda functions")
336
-
337
- # Normalize task to TaskConfig and validate
338
- if task is None:
339
- task_config = TaskConfig(mode="forbidden")
340
- elif isinstance(task, bool):
341
- task_config = TaskConfig.from_bool(task)
342
- else:
343
- task_config = task
344
- task_config.validate_function(fn, func_name)
345
-
346
- if isinstance(output_schema, NotSetT):
347
- final_output_schema = parsed_fn.output_schema
348
- else:
349
- # At this point output_schema is not NotSetT, so it must be dict | None
350
- final_output_schema = output_schema
351
- # Note: explicit schemas (dict) are used as-is without auto-wrapping
352
-
353
- # Validate that explicit schemas are object type for structured content
354
- # (resolving $ref references for self-referencing types)
355
- if final_output_schema is not None and isinstance(final_output_schema, dict):
356
- if not _is_object_schema(final_output_schema):
357
- raise ValueError(
358
- f"Output schemas must represent object types due to MCP spec limitations. Received: {final_output_schema!r}"
359
- )
360
-
361
- return cls(
362
- fn=parsed_fn.fn,
363
- name=name or parsed_fn.name,
364
- title=title,
365
- description=description or parsed_fn.description,
366
- icons=icons,
367
- parameters=parsed_fn.input_schema,
368
- output_schema=final_output_schema,
369
- annotations=annotations,
370
- tags=tags or set(),
371
- serializer=serializer,
372
- meta=meta,
373
- enabled=enabled if enabled is not None else True,
374
- task_config=task_config,
375
- )
376
-
377
- async def run(self, arguments: dict[str, Any]) -> ToolResult:
378
- """Run the tool with arguments."""
379
- wrapper_fn = without_injected_parameters(self.fn)
380
- type_adapter = get_cached_typeadapter(wrapper_fn)
381
- result = type_adapter.validate_python(arguments)
382
- if inspect.isawaitable(result):
383
- result = await result
384
-
385
- if isinstance(result, ToolResult):
386
- return result
387
-
388
- unstructured_result = _convert_to_content(result, serializer=self.serializer)
389
-
390
- if self.output_schema is None:
391
- # Do not produce a structured output for MCP Content Types
392
- if isinstance(result, ContentBlock | Audio | Image | File) or (
393
- isinstance(result, list | tuple)
394
- and any(isinstance(item, ContentBlock) for item in result)
395
- ):
396
- return ToolResult(content=unstructured_result)
397
-
398
- # Otherwise, try to serialize the result as a dict
399
- try:
400
- structured_content = pydantic_core.to_jsonable_python(result)
401
- if isinstance(structured_content, dict):
402
- return ToolResult(
403
- content=unstructured_result,
404
- structured_content=structured_content,
405
- )
406
-
407
- except pydantic_core.PydanticSerializationError:
408
- pass
409
-
410
- return ToolResult(content=unstructured_result)
411
-
412
- wrap_result = self.output_schema.get("x-fastmcp-wrap-result")
413
-
414
- return ToolResult(
415
- content=unstructured_result,
416
- structured_content={"result": result} if wrap_result else result,
417
- )
418
-
419
-
420
- def _is_object_schema(schema: dict[str, Any]) -> bool:
421
- """Check if a JSON schema represents an object type."""
422
- # Direct object type
423
- if schema.get("type") == "object":
424
- return True
425
-
426
- # Schema with properties but no explicit type is treated as object
427
- if "properties" in schema:
428
- return True
429
-
430
- # Self-referencing types use $ref pointing to $defs
431
- # The referenced type is always an object in our use case
432
- return "$ref" in schema and "$defs" in schema
433
-
434
-
435
- @dataclass
436
- class ParsedFunction:
437
- fn: Callable[..., Any]
438
- name: str
439
- description: str | None
440
- input_schema: dict[str, Any]
441
- output_schema: dict[str, Any] | None
442
-
443
- @classmethod
444
- def from_function(
445
- cls,
446
- fn: Callable[..., Any],
447
- exclude_args: list[str] | None = None,
448
- validate: bool = True,
449
- wrap_non_object_output_schema: bool = True,
450
- ) -> ParsedFunction:
451
- if validate:
452
- sig = inspect.signature(fn)
453
- # Reject functions with *args or **kwargs
454
- for param in sig.parameters.values():
455
- if param.kind == inspect.Parameter.VAR_POSITIONAL:
456
- raise ValueError("Functions with *args are not supported as tools")
457
- if param.kind == inspect.Parameter.VAR_KEYWORD:
458
- raise ValueError(
459
- "Functions with **kwargs are not supported as tools"
460
- )
461
-
462
- # Reject exclude_args that don't exist in the function or don't have a default value
463
- if exclude_args:
464
- for arg_name in exclude_args:
465
- if arg_name not in sig.parameters:
466
- raise ValueError(
467
- f"Parameter '{arg_name}' in exclude_args does not exist in function."
468
- )
469
- param = sig.parameters[arg_name]
470
- if param.default == inspect.Parameter.empty:
471
- raise ValueError(
472
- f"Parameter '{arg_name}' in exclude_args must have a default value."
473
- )
474
-
475
- # collect name and doc before we potentially modify the function
476
- fn_name = getattr(fn, "__name__", None) or fn.__class__.__name__
477
- fn_doc = inspect.getdoc(fn)
478
-
479
- # if the fn is a callable class, we need to get the __call__ method from here out
480
- if not inspect.isroutine(fn):
481
- fn = fn.__call__
482
- # if the fn is a staticmethod, we need to work with the underlying function
483
- if isinstance(fn, staticmethod):
484
- fn = fn.__func__
485
-
486
- # Handle injected parameters (Context, Docket dependencies)
487
- wrapper_fn = without_injected_parameters(fn)
488
-
489
- # Also handle exclude_args with non-serializable types (issue #2431)
490
- # This must happen before Pydantic tries to serialize the parameters
491
- if exclude_args:
492
- wrapper_fn = create_function_without_params(wrapper_fn, list(exclude_args))
493
-
494
- input_type_adapter = get_cached_typeadapter(wrapper_fn)
495
- input_schema = input_type_adapter.json_schema()
496
-
497
- # Compress and handle exclude_args
498
- prune_params = list(exclude_args) if exclude_args else None
499
- input_schema = compress_schema(
500
- input_schema, prune_params=prune_params, prune_titles=True
501
- )
502
-
503
- output_schema = None
504
- # Get the return annotation from the signature
505
- sig = inspect.signature(fn)
506
- output_type = sig.return_annotation
507
-
508
- # If the annotation is a string (from __future__ annotations), resolve it
509
- if isinstance(output_type, str):
510
- try:
511
- # Use get_type_hints to resolve the return type
512
- # include_extras=True preserves Annotated metadata
513
- type_hints = get_type_hints(fn, include_extras=True)
514
- output_type = type_hints.get("return", output_type)
515
- except Exception:
516
- # If resolution fails, keep the string annotation
517
- pass
518
-
519
- if output_type not in (inspect._empty, None, Any, ...):
520
- # there are a variety of types that we don't want to attempt to
521
- # serialize because they are either used by FastMCP internally,
522
- # or are MCP content types that explicitly don't form structured
523
- # content. By replacing them with an explicitly unserializable type,
524
- # we ensure that no output schema is automatically generated.
525
- clean_output_type = replace_type(
526
- output_type,
527
- dict.fromkeys( # type: ignore[arg-type]
528
- (
529
- Image,
530
- Audio,
531
- File,
532
- ToolResult,
533
- mcp.types.TextContent,
534
- mcp.types.ImageContent,
535
- mcp.types.AudioContent,
536
- mcp.types.ResourceLink,
537
- mcp.types.EmbeddedResource,
538
- ),
539
- _UnserializableType,
540
- ),
541
- )
542
-
543
- try:
544
- type_adapter = get_cached_typeadapter(clean_output_type)
545
- base_schema = type_adapter.json_schema(mode="serialization")
546
-
547
- # Generate schema for wrapped type if it's non-object
548
- # because MCP requires that output schemas are objects
549
- # Check if schema is an object type, resolving $ref references
550
- # (self-referencing types use $ref at root level)
551
- if wrap_non_object_output_schema and not _is_object_schema(base_schema):
552
- # Use the wrapped result schema directly
553
- wrapped_type = _WrappedResult[clean_output_type]
554
- wrapped_adapter = get_cached_typeadapter(wrapped_type)
555
- output_schema = wrapped_adapter.json_schema(mode="serialization")
556
- output_schema["x-fastmcp-wrap-result"] = True
557
- else:
558
- output_schema = base_schema
559
-
560
- output_schema = compress_schema(output_schema, prune_titles=True)
561
-
562
- # Resolve root-level $ref to meet MCP spec requirement for type: object
563
- # Self-referential Pydantic models generate schemas with $ref at root
564
- output_schema = resolve_root_ref(output_schema)
565
-
566
- except PydanticSchemaGenerationError as e:
567
- if "_UnserializableType" not in str(e):
568
- logger.debug(f"Unable to generate schema for type {output_type!r}")
569
-
570
- return cls(
571
- fn=fn,
572
- name=fn_name,
573
- description=fn_doc,
574
- input_schema=input_schema,
575
- output_schema=output_schema or None,
576
- )
397
+ def get_span_attributes(self) -> dict[str, Any]:
398
+ return super().get_span_attributes() | {
399
+ "fastmcp.component.type": "tool",
400
+ "fastmcp.provider.type": "LocalProvider",
401
+ }
577
402
 
578
403
 
579
404
  def _serialize_with_fallback(
@@ -641,3 +466,31 @@ def _convert_to_content(
641
466
  ]
642
467
  # If none of the items are ContentBlocks, aggregate all items into a single TextContent
643
468
  return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
469
+
470
+
471
+ __all__ = ["Tool", "ToolResult"]
472
+
473
+
474
+ def __getattr__(name: str) -> Any:
475
+ """Deprecated re-exports for backwards compatibility."""
476
+ deprecated_exports = {
477
+ "FunctionTool": "FunctionTool",
478
+ "ParsedFunction": "ParsedFunction",
479
+ "tool": "tool",
480
+ }
481
+
482
+ if name in deprecated_exports:
483
+ import fastmcp
484
+
485
+ if fastmcp.settings.deprecation_warnings:
486
+ warnings.warn(
487
+ f"Importing {name} from fastmcp.tools.tool is deprecated. "
488
+ f"Import from fastmcp.tools.function_tool instead.",
489
+ DeprecationWarning,
490
+ stacklevel=2,
491
+ )
492
+ from fastmcp.tools import function_tool
493
+
494
+ return getattr(function_tool, name)
495
+
496
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")