fastmcp 2.10.6__py3-none-any.whl → 2.11.0__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 (61) hide show
  1. fastmcp/cli/cli.py +128 -33
  2. fastmcp/cli/install/claude_code.py +42 -1
  3. fastmcp/cli/install/claude_desktop.py +42 -1
  4. fastmcp/cli/install/cursor.py +42 -1
  5. fastmcp/cli/install/mcp_json.py +41 -0
  6. fastmcp/cli/run.py +127 -1
  7. fastmcp/client/__init__.py +2 -0
  8. fastmcp/client/auth/oauth.py +68 -99
  9. fastmcp/client/oauth_callback.py +18 -0
  10. fastmcp/client/transports.py +69 -15
  11. fastmcp/contrib/component_manager/example.py +2 -2
  12. fastmcp/experimental/server/openapi/README.md +266 -0
  13. fastmcp/experimental/server/openapi/__init__.py +38 -0
  14. fastmcp/experimental/server/openapi/components.py +348 -0
  15. fastmcp/experimental/server/openapi/routing.py +132 -0
  16. fastmcp/experimental/server/openapi/server.py +466 -0
  17. fastmcp/experimental/utilities/openapi/README.md +239 -0
  18. fastmcp/experimental/utilities/openapi/__init__.py +68 -0
  19. fastmcp/experimental/utilities/openapi/director.py +208 -0
  20. fastmcp/experimental/utilities/openapi/formatters.py +355 -0
  21. fastmcp/experimental/utilities/openapi/json_schema_converter.py +340 -0
  22. fastmcp/experimental/utilities/openapi/models.py +85 -0
  23. fastmcp/experimental/utilities/openapi/parser.py +618 -0
  24. fastmcp/experimental/utilities/openapi/schemas.py +538 -0
  25. fastmcp/mcp_config.py +125 -88
  26. fastmcp/prompts/prompt.py +11 -1
  27. fastmcp/resources/resource.py +21 -1
  28. fastmcp/resources/template.py +20 -1
  29. fastmcp/server/auth/__init__.py +17 -2
  30. fastmcp/server/auth/auth.py +144 -7
  31. fastmcp/server/auth/providers/bearer.py +25 -473
  32. fastmcp/server/auth/providers/in_memory.py +4 -2
  33. fastmcp/server/auth/providers/jwt.py +538 -0
  34. fastmcp/server/auth/providers/workos.py +170 -0
  35. fastmcp/server/auth/registry.py +52 -0
  36. fastmcp/server/context.py +107 -26
  37. fastmcp/server/dependencies.py +9 -2
  38. fastmcp/server/http.py +62 -30
  39. fastmcp/server/middleware/middleware.py +3 -23
  40. fastmcp/server/openapi.py +1 -1
  41. fastmcp/server/proxy.py +50 -11
  42. fastmcp/server/server.py +168 -59
  43. fastmcp/settings.py +73 -6
  44. fastmcp/tools/tool.py +36 -3
  45. fastmcp/tools/tool_manager.py +38 -2
  46. fastmcp/tools/tool_transform.py +112 -3
  47. fastmcp/utilities/components.py +35 -2
  48. fastmcp/utilities/json_schema.py +136 -98
  49. fastmcp/utilities/json_schema_type.py +1 -3
  50. fastmcp/utilities/mcp_config.py +28 -0
  51. fastmcp/utilities/openapi.py +240 -50
  52. fastmcp/utilities/tests.py +54 -6
  53. fastmcp/utilities/types.py +89 -11
  54. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/METADATA +4 -3
  55. fastmcp-2.11.0.dist-info/RECORD +108 -0
  56. fastmcp/server/auth/providers/bearer_env.py +0 -63
  57. fastmcp/utilities/cache.py +0 -26
  58. fastmcp-2.10.6.dist-info/RECORD +0 -93
  59. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/WHEEL +0 -0
  60. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/entry_points.txt +0 -0
  61. {fastmcp-2.10.6.dist-info → fastmcp-2.11.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/tools/tool.py CHANGED
@@ -3,7 +3,15 @@ from __future__ import annotations
3
3
  import inspect
4
4
  from collections.abc import Callable
5
5
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Annotated, Any, Generic, Literal, TypeVar
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Annotated,
9
+ Any,
10
+ Generic,
11
+ Literal,
12
+ TypeVar,
13
+ get_type_hints,
14
+ )
7
15
 
8
16
  import mcp.types
9
17
  import pydantic_core
@@ -122,7 +130,12 @@ class Tool(FastMCPComponent):
122
130
  except RuntimeError:
123
131
  pass # No context available
124
132
 
125
- def to_mcp_tool(self, **overrides: Any) -> MCPTool:
133
+ def to_mcp_tool(
134
+ self,
135
+ *,
136
+ include_fastmcp_meta: bool | None = None,
137
+ **overrides: Any,
138
+ ) -> MCPTool:
126
139
  if self.title:
127
140
  title = self.title
128
141
  elif self.annotations and self.annotations.title:
@@ -137,6 +150,7 @@ class Tool(FastMCPComponent):
137
150
  "outputSchema": self.output_schema,
138
151
  "annotations": self.annotations,
139
152
  "title": title,
153
+ "_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
140
154
  }
141
155
  return MCPTool(**kwargs | overrides)
142
156
 
@@ -151,6 +165,7 @@ class Tool(FastMCPComponent):
151
165
  exclude_args: list[str] | None = None,
152
166
  output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
153
167
  serializer: Callable[[Any], str] | None = None,
168
+ meta: dict[str, Any] | None = None,
154
169
  enabled: bool | None = None,
155
170
  ) -> FunctionTool:
156
171
  """Create a Tool from a function."""
@@ -164,6 +179,7 @@ class Tool(FastMCPComponent):
164
179
  exclude_args=exclude_args,
165
180
  output_schema=output_schema,
166
181
  serializer=serializer,
182
+ meta=meta,
167
183
  enabled=enabled,
168
184
  )
169
185
 
@@ -192,6 +208,7 @@ class Tool(FastMCPComponent):
192
208
  annotations: ToolAnnotations | None = None,
193
209
  output_schema: dict[str, Any] | None | Literal[False] = None,
194
210
  serializer: Callable[[Any], str] | None = None,
211
+ meta: dict[str, Any] | None | NotSetT = NotSet,
195
212
  enabled: bool | None = None,
196
213
  ) -> TransformedTool:
197
214
  from fastmcp.tools.tool_transform import TransformedTool
@@ -207,6 +224,7 @@ class Tool(FastMCPComponent):
207
224
  annotations=annotations,
208
225
  output_schema=output_schema,
209
226
  serializer=serializer,
227
+ meta=meta,
210
228
  enabled=enabled,
211
229
  )
212
230
 
@@ -226,6 +244,7 @@ class FunctionTool(Tool):
226
244
  exclude_args: list[str] | None = None,
227
245
  output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
228
246
  serializer: Callable[[Any], str] | None = None,
247
+ meta: dict[str, Any] | None = None,
229
248
  enabled: bool | None = None,
230
249
  ) -> FunctionTool:
231
250
  """Create a Tool from a function."""
@@ -258,6 +277,7 @@ class FunctionTool(Tool):
258
277
  annotations=annotations,
259
278
  tags=tags or set(),
260
279
  serializer=serializer,
280
+ meta=meta,
261
281
  enabled=enabled if enabled is not None else True,
262
282
  )
263
283
 
@@ -371,7 +391,20 @@ class ParsedFunction:
371
391
  input_schema = compress_schema(input_schema, prune_params=prune_params)
372
392
 
373
393
  output_schema = None
374
- output_type = inspect.signature(fn).return_annotation
394
+ # Get the return annotation from the signature
395
+ sig = inspect.signature(fn)
396
+ output_type = sig.return_annotation
397
+
398
+ # If the annotation is a string (from __future__ annotations), resolve it
399
+ if isinstance(output_type, str):
400
+ try:
401
+ # Use get_type_hints to resolve the return type
402
+ # include_extras=True preserves Annotated metadata
403
+ type_hints = get_type_hints(fn, include_extras=True)
404
+ output_type = type_hints.get("return", output_type)
405
+ except Exception:
406
+ # If resolution fails, keep the string annotation
407
+ pass
375
408
 
376
409
  if output_type not in (inspect._empty, None, Any, ...):
377
410
  # there are a variety of types that we don't want to attempt to
@@ -10,6 +10,10 @@ from fastmcp import settings
10
10
  from fastmcp.exceptions import NotFoundError, ToolError
11
11
  from fastmcp.settings import DuplicateBehavior
12
12
  from fastmcp.tools.tool import Tool, ToolResult
13
+ from fastmcp.tools.tool_transform import (
14
+ ToolTransformConfig,
15
+ apply_transformations_to_tools,
16
+ )
13
17
  from fastmcp.utilities.logging import get_logger
14
18
 
15
19
  if TYPE_CHECKING:
@@ -25,10 +29,12 @@ class ToolManager:
25
29
  self,
26
30
  duplicate_behavior: DuplicateBehavior | None = None,
27
31
  mask_error_details: bool | None = None,
32
+ transformations: dict[str, ToolTransformConfig] | None = None,
28
33
  ):
29
34
  self._tools: dict[str, Tool] = {}
30
35
  self._mounted_servers: list[MountedServer] = []
31
36
  self.mask_error_details = mask_error_details or settings.mask_error_details
37
+ self.transformations = transformations or {}
32
38
 
33
39
  # Default to "warn" if None is provided
34
40
  if duplicate_behavior is None:
@@ -82,7 +88,13 @@ class ToolManager:
82
88
 
83
89
  # Finally, add local tools, which always take precedence
84
90
  all_tools.update(self._tools)
85
- return all_tools
91
+
92
+ transformed_tools = apply_transformations_to_tools(
93
+ tools=all_tools,
94
+ transformations=self.transformations,
95
+ )
96
+
97
+ return transformed_tools
86
98
 
87
99
  async def has_tool(self, key: str) -> bool:
88
100
  """Check if a tool exists."""
@@ -109,6 +121,15 @@ class ToolManager:
109
121
  tools_dict = await self._load_tools(via_server=True)
110
122
  return list(tools_dict.values())
111
123
 
124
+ @property
125
+ def _tools_transformed(self) -> list[str]:
126
+ """Get the local tools."""
127
+
128
+ return [
129
+ transformation.name or tool_name
130
+ for tool_name, transformation in self.transformations.items()
131
+ ]
132
+
112
133
  def add_tool_from_fn(
113
134
  self,
114
135
  fn: Callable[..., Any],
@@ -155,6 +176,21 @@ class ToolManager:
155
176
  self._tools[tool.key] = tool
156
177
  return tool
157
178
 
179
+ def add_tool_transformation(
180
+ self, tool_name: str, transformation: ToolTransformConfig
181
+ ) -> None:
182
+ """Add a tool transformation."""
183
+ self.transformations[tool_name] = transformation
184
+
185
+ def get_tool_transformation(self, tool_name: str) -> ToolTransformConfig | None:
186
+ """Get a tool transformation."""
187
+ return self.transformations.get(tool_name)
188
+
189
+ def remove_tool_transformation(self, tool_name: str) -> None:
190
+ """Remove a tool transformation."""
191
+ if tool_name in self.transformations:
192
+ del self.transformations[tool_name]
193
+
158
194
  def remove_tool(self, key: str) -> None:
159
195
  """Remove a tool from the server.
160
196
 
@@ -175,7 +211,7 @@ class ToolManager:
175
211
  filtered protocol path.
176
212
  """
177
213
  # 1. Check local tools first. The server will have already applied its filter.
178
- if key in self._tools:
214
+ if key in self._tools or key in self._tools_transformed:
179
215
  tool = await self.get_tool(key)
180
216
  if not tool:
181
217
  raise NotFoundError(f"Tool {key!r} not found")
@@ -4,14 +4,23 @@ import inspect
4
4
  from collections.abc import Callable
5
5
  from contextvars import ContextVar
6
6
  from dataclasses import dataclass
7
- from typing import Any, Literal
7
+ from typing import Annotated, Any, Literal
8
8
 
9
9
  from mcp.types import ToolAnnotations
10
10
  from pydantic import ConfigDict
11
+ from pydantic.fields import Field
12
+ from pydantic.functional_validators import BeforeValidator
11
13
 
12
14
  from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
15
+ from fastmcp.utilities.components import _convert_set_default_none
16
+ from fastmcp.utilities.json_schema import compress_schema
13
17
  from fastmcp.utilities.logging import get_logger
14
- from fastmcp.utilities.types import NotSet, NotSetT, get_cached_typeadapter
18
+ from fastmcp.utilities.types import (
19
+ FastMCPBaseModel,
20
+ NotSet,
21
+ NotSetT,
22
+ get_cached_typeadapter,
23
+ )
15
24
 
16
25
  logger = get_logger(__name__)
17
26
 
@@ -193,6 +202,30 @@ class ArgTransform:
193
202
  )
194
203
 
195
204
 
205
+ class ArgTransformConfig(FastMCPBaseModel):
206
+ """A model for requesting a single argument transform."""
207
+
208
+ name: str | None = Field(default=None, description="The new name for the argument.")
209
+ description: str | None = Field(
210
+ default=None, description="The new description for the argument."
211
+ )
212
+ default: str | int | float | bool | None = Field(
213
+ default=None, description="The new default value for the argument."
214
+ )
215
+ hide: bool = Field(
216
+ default=False, description="Whether to hide the argument from the tool."
217
+ )
218
+ required: Literal[True] | None = Field(
219
+ default=None, description="Whether the argument is required."
220
+ )
221
+ examples: Any | None = Field(default=None, description="Examples of the argument.")
222
+
223
+ def to_arg_transform(self) -> ArgTransform:
224
+ """Convert the argument transform to a FastMCP argument transform."""
225
+
226
+ return ArgTransform(**self.model_dump(exclude_unset=True)) # pyright: ignore[reportAny]
227
+
228
+
196
229
  class TransformedTool(Tool):
197
230
  """A tool that is transformed from another tool.
198
231
 
@@ -333,6 +366,7 @@ class TransformedTool(Tool):
333
366
  annotations: ToolAnnotations | None = None,
334
367
  output_schema: dict[str, Any] | None | Literal[False] = None,
335
368
  serializer: Callable[[Any], str] | None = None,
369
+ meta: dict[str, Any] | None | NotSetT = NotSet,
336
370
  enabled: bool | None = None,
337
371
  ) -> TransformedTool:
338
372
  """Create a transformed tool from a parent tool.
@@ -357,6 +391,10 @@ class TransformedTool(Tool):
357
391
  - dict: Use custom output schema
358
392
  - False: Disable output schema and structured outputs
359
393
  serializer: New serializer. Defaults to parent's serializer.
394
+ meta: Control meta information:
395
+ - NotSet (default): Inherit from parent tool
396
+ - dict: Use custom meta information
397
+ - None: Remove meta information
360
398
 
361
399
  Returns:
362
400
  TransformedTool with the specified transformations.
@@ -413,7 +451,7 @@ class TransformedTool(Tool):
413
451
  if unknown_args:
414
452
  raise ValueError(
415
453
  f"Unknown arguments in transform_args: {', '.join(sorted(unknown_args))}. "
416
- f"Parent tool has: {', '.join(sorted(parent_params))}"
454
+ f"Parent tool `{tool.name}` has: {', '.join(sorted(parent_params))}"
417
455
  )
418
456
 
419
457
  # Always create the forwarding transform
@@ -513,6 +551,7 @@ class TransformedTool(Tool):
513
551
  description if not isinstance(description, NotSetT) else tool.description
514
552
  )
515
553
  final_title = title if not isinstance(title, NotSetT) else tool.title
554
+ final_meta = meta if not isinstance(meta, NotSetT) else tool.meta
516
555
 
517
556
  transformed_tool = cls(
518
557
  fn=final_fn,
@@ -526,6 +565,7 @@ class TransformedTool(Tool):
526
565
  tags=tags or tool.tags,
527
566
  annotations=annotations or tool.annotations,
528
567
  serializer=serializer or tool.serializer,
568
+ meta=final_meta,
529
569
  transform_args=transform_args,
530
570
  enabled=enabled if enabled is not None else True,
531
571
  )
@@ -613,6 +653,7 @@ class TransformedTool(Tool):
613
653
 
614
654
  if parent_defs:
615
655
  schema["$defs"] = parent_defs
656
+ schema = compress_schema(schema, prune_defs=True)
616
657
 
617
658
  # Create forwarding function that closes over everything it needs
618
659
  async def _forward(**kwargs):
@@ -798,3 +839,71 @@ class TransformedTool(Tool):
798
839
  return any(
799
840
  p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
800
841
  )
842
+
843
+
844
+ class ToolTransformConfig(FastMCPBaseModel):
845
+ """Provides a way to transform a tool."""
846
+
847
+ name: str | None = Field(default=None, description="The new name for the tool.")
848
+
849
+ title: str | None = Field(
850
+ default=None,
851
+ description="The new title of the tool.",
852
+ )
853
+ description: str | None = Field(
854
+ default=None,
855
+ description="The new description of the tool.",
856
+ )
857
+ tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(
858
+ default_factory=set,
859
+ description="The new tags for the tool.",
860
+ )
861
+ meta: dict[str, Any] | None = Field(
862
+ default=None,
863
+ description="The new meta information for the tool.",
864
+ )
865
+
866
+ enabled: bool = Field(
867
+ default=True,
868
+ description="Whether the tool is enabled.",
869
+ )
870
+
871
+ arguments: dict[str, ArgTransformConfig] = Field(
872
+ default_factory=dict,
873
+ description="A dictionary of argument transforms to apply to the tool.",
874
+ )
875
+
876
+ def apply(self, tool: Tool) -> TransformedTool:
877
+ """Create a TransformedTool from a provided tool and this transformation configuration."""
878
+
879
+ tool_changes: dict[str, Any] = self.model_dump(
880
+ exclude_unset=True, exclude={"arguments"}
881
+ )
882
+
883
+ return TransformedTool.from_tool(
884
+ tool=tool,
885
+ **tool_changes,
886
+ transform_args={k: v.to_arg_transform() for k, v in self.arguments.items()},
887
+ )
888
+
889
+
890
+ def apply_transformations_to_tools(
891
+ tools: dict[str, Tool],
892
+ transformations: dict[str, ToolTransformConfig],
893
+ ) -> dict[str, Tool]:
894
+ """Apply a list of transformations to a list of tools. Tools that do not have any transforamtions
895
+ are left unchanged.
896
+ """
897
+
898
+ transformed_tools: dict[str, Tool] = {}
899
+
900
+ for tool_name, tool in tools.items():
901
+ if transformation := transformations.get(tool_name):
902
+ transformed_tools[transformation.name or tool_name] = transformation.apply(
903
+ tool
904
+ )
905
+ continue
906
+
907
+ transformed_tools[tool_name] = tool
908
+
909
+ return transformed_tools
@@ -1,14 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  from collections.abc import Sequence
2
- from typing import Annotated, Any, TypeVar
4
+ from typing import Annotated, Any, TypedDict, TypeVar
3
5
 
4
6
  from pydantic import BeforeValidator, Field, PrivateAttr
5
7
  from typing_extensions import Self
6
8
 
9
+ import fastmcp
7
10
  from fastmcp.utilities.types import FastMCPBaseModel
8
11
 
9
12
  T = TypeVar("T")
10
13
 
11
14
 
15
+ class FastMCPMeta(TypedDict, total=False):
16
+ tags: list[str]
17
+
18
+
12
19
  def _convert_set_default_none(maybe_set: set[T] | Sequence[T] | None) -> set[T]:
13
20
  """Convert a sequence to a set, defaulting to an empty set if None."""
14
21
  if maybe_set is None:
@@ -36,7 +43,9 @@ class FastMCPComponent(FastMCPBaseModel):
36
43
  default_factory=set,
37
44
  description="Tags for the component.",
38
45
  )
39
-
46
+ meta: dict[str, Any] | None = Field(
47
+ default=None, description="Meta information about the component"
48
+ )
40
49
  enabled: bool = Field(
41
50
  default=True,
42
51
  description="Whether the component is enabled.",
@@ -58,6 +67,30 @@ class FastMCPComponent(FastMCPBaseModel):
58
67
  """
59
68
  return self._key or self.name
60
69
 
70
+ def get_meta(
71
+ self, include_fastmcp_meta: bool | None = None
72
+ ) -> dict[str, Any] | None:
73
+ """
74
+ Get the meta information about the component.
75
+
76
+ If include_fastmcp_meta is True, a `_fastmcp` key will be added to the
77
+ meta, containing a `tags` field with the tags of the component.
78
+ """
79
+
80
+ if include_fastmcp_meta is None:
81
+ include_fastmcp_meta = fastmcp.settings.include_fastmcp_meta
82
+
83
+ meta = self.meta or {}
84
+
85
+ if include_fastmcp_meta:
86
+ fastmcp_meta = FastMCPMeta(tags=sorted(self.tags))
87
+ # overwrite any existing _fastmcp meta with keys from the new one
88
+ if upstream_meta := meta.get("_fastmcp"):
89
+ fastmcp_meta = upstream_meta | fastmcp_meta
90
+ meta["_fastmcp"] = fastmcp_meta
91
+
92
+ return meta or None
93
+
61
94
  def with_key(self, key: str) -> Self:
62
95
  return self.model_copy(update={"_key": key})
63
96