fastmcp 2.10.4__py3-none-any.whl → 2.10.6__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.
fastmcp/server/server.py CHANGED
@@ -759,7 +759,7 @@ class FastMCP(Generic[LifespanResultT]):
759
759
  )
760
760
  return await self._apply_middleware(mw_context, _handler)
761
761
 
762
- def add_tool(self, tool: Tool) -> None:
762
+ def add_tool(self, tool: Tool) -> Tool:
763
763
  """Add a tool to the server.
764
764
 
765
765
  The tool function can optionally request a Context object by adding a parameter
@@ -767,6 +767,9 @@ class FastMCP(Generic[LifespanResultT]):
767
767
 
768
768
  Args:
769
769
  tool: The Tool instance to register
770
+
771
+ Returns:
772
+ The tool instance that was added to the server.
770
773
  """
771
774
  self._tool_manager.add_tool(tool)
772
775
  self._cache.clear()
@@ -780,6 +783,8 @@ class FastMCP(Generic[LifespanResultT]):
780
783
  except RuntimeError:
781
784
  pass # No context available
782
785
 
786
+ return tool
787
+
783
788
  def remove_tool(self, name: str) -> None:
784
789
  """Remove a tool from the server.
785
790
 
@@ -958,13 +963,15 @@ class FastMCP(Generic[LifespanResultT]):
958
963
  enabled=enabled,
959
964
  )
960
965
 
961
- def add_resource(self, resource: Resource) -> None:
966
+ def add_resource(self, resource: Resource) -> Resource:
962
967
  """Add a resource to the server.
963
968
 
964
969
  Args:
965
970
  resource: A Resource instance to add
966
- """
967
971
 
972
+ Returns:
973
+ The resource instance that was added to the server.
974
+ """
968
975
  self._resource_manager.add_resource(resource)
969
976
  self._cache.clear()
970
977
 
@@ -977,11 +984,16 @@ class FastMCP(Generic[LifespanResultT]):
977
984
  except RuntimeError:
978
985
  pass # No context available
979
986
 
980
- def add_template(self, template: ResourceTemplate) -> None:
987
+ return resource
988
+
989
+ def add_template(self, template: ResourceTemplate) -> ResourceTemplate:
981
990
  """Add a resource template to the server.
982
991
 
983
992
  Args:
984
993
  template: A ResourceTemplate instance to add
994
+
995
+ Returns:
996
+ The template instance that was added to the server.
985
997
  """
986
998
  self._resource_manager.add_template(template)
987
999
 
@@ -994,6 +1006,8 @@ class FastMCP(Generic[LifespanResultT]):
994
1006
  except RuntimeError:
995
1007
  pass # No context available
996
1008
 
1009
+ return template
1010
+
997
1011
  def add_resource_fn(
998
1012
  self,
999
1013
  fn: AnyFunction,
@@ -1159,11 +1173,14 @@ class FastMCP(Generic[LifespanResultT]):
1159
1173
 
1160
1174
  return decorator
1161
1175
 
1162
- def add_prompt(self, prompt: Prompt) -> None:
1176
+ def add_prompt(self, prompt: Prompt) -> Prompt:
1163
1177
  """Add a prompt to the server.
1164
1178
 
1165
1179
  Args:
1166
1180
  prompt: A Prompt instance to add
1181
+
1182
+ Returns:
1183
+ The prompt instance that was added to the server.
1167
1184
  """
1168
1185
  self._prompt_manager.add_prompt(prompt)
1169
1186
  self._cache.clear()
@@ -1177,6 +1194,8 @@ class FastMCP(Generic[LifespanResultT]):
1177
1194
  except RuntimeError:
1178
1195
  pass # No context available
1179
1196
 
1197
+ return prompt
1198
+
1180
1199
  @overload
1181
1200
  def prompt(
1182
1201
  self,
@@ -1643,8 +1662,7 @@ class FastMCP(Generic[LifespanResultT]):
1643
1662
  resource_separator: Deprecated. Separator character for resource URIs.
1644
1663
  prompt_separator: Deprecated. Separator character for prompt names.
1645
1664
  """
1646
- from fastmcp.client.transports import FastMCPTransport
1647
- from fastmcp.server.proxy import FastMCPProxy, ProxyClient
1665
+ from fastmcp.server.proxy import FastMCPProxy
1648
1666
 
1649
1667
  # Deprecated since 2.9.0
1650
1668
  # Prior to 2.9.0, the first positional argument was the prefix and the
@@ -1696,7 +1714,7 @@ class FastMCP(Generic[LifespanResultT]):
1696
1714
  as_proxy = server._has_lifespan
1697
1715
 
1698
1716
  if as_proxy and not isinstance(server, FastMCPProxy):
1699
- server = FastMCPProxy(ProxyClient(transport=FastMCPTransport(server)))
1717
+ server = FastMCP.as_proxy(server)
1700
1718
 
1701
1719
  # Delegate mounting to all three managers
1702
1720
  mounted_server = MountedServer(
fastmcp/settings.py CHANGED
@@ -5,7 +5,7 @@ import warnings
5
5
  from pathlib import Path
6
6
  from typing import Annotated, Any, Literal
7
7
 
8
- from pydantic import Field, model_validator
8
+ from pydantic import Field, field_validator
9
9
  from pydantic.fields import FieldInfo
10
10
  from pydantic_settings import (
11
11
  BaseSettings,
@@ -99,7 +99,16 @@ class Settings(BaseSettings):
99
99
  home: Path = Path.home() / ".fastmcp"
100
100
 
101
101
  test_mode: bool = False
102
+
102
103
  log_level: LOG_LEVEL = "INFO"
104
+
105
+ @field_validator("log_level", mode="before")
106
+ @classmethod
107
+ def normalize_log_level(cls, v):
108
+ if isinstance(v, str):
109
+ return v.upper()
110
+ return v
111
+
103
112
  enable_rich_tracebacks: Annotated[
104
113
  bool,
105
114
  Field(
@@ -162,17 +171,6 @@ class Settings(BaseSettings):
162
171
  ),
163
172
  ] = None
164
173
 
165
- @model_validator(mode="after")
166
- def setup_logging(self) -> Self:
167
- """Finalize the settings."""
168
- from fastmcp.utilities.logging import configure_logging
169
-
170
- configure_logging(
171
- self.log_level, enable_rich_tracebacks=self.enable_rich_tracebacks
172
- )
173
-
174
- return self
175
-
176
174
  # HTTP settings
177
175
  host: str = "127.0.0.1"
178
176
  port: int = 8000
fastmcp/tools/tool.py CHANGED
@@ -185,8 +185,9 @@ class Tool(FastMCPComponent):
185
185
  tool: Tool,
186
186
  transform_fn: Callable[..., Any] | None = None,
187
187
  name: str | None = None,
188
+ title: str | None | NotSetT = NotSet,
188
189
  transform_args: dict[str, ArgTransform] | None = None,
189
- description: str | None = None,
190
+ description: str | None | NotSetT = NotSet,
190
191
  tags: set[str] | None = None,
191
192
  annotations: ToolAnnotations | None = None,
192
193
  output_schema: dict[str, Any] | None | Literal[False] = None,
@@ -199,6 +200,7 @@ class Tool(FastMCPComponent):
199
200
  tool=tool,
200
201
  transform_fn=transform_fn,
201
202
  name=name,
203
+ title=title,
202
204
  transform_args=transform_args,
203
205
  description=description,
204
206
  tags=tags,
@@ -397,7 +399,7 @@ class ParsedFunction:
397
399
 
398
400
  try:
399
401
  type_adapter = get_cached_typeadapter(clean_output_type)
400
- base_schema = type_adapter.json_schema()
402
+ base_schema = type_adapter.json_schema(mode="serialization")
401
403
 
402
404
  # Generate schema for wrapped type if it's non-object
403
405
  # because MCP requires that output schemas are objects
@@ -408,7 +410,7 @@ class ParsedFunction:
408
410
  # Use the wrapped result schema directly
409
411
  wrapped_type = _WrappedResult[clean_output_type]
410
412
  wrapped_adapter = get_cached_typeadapter(wrapped_type)
411
- output_schema = wrapped_adapter.json_schema()
413
+ output_schema = wrapped_adapter.json_schema(mode="serialization")
412
414
  output_schema["x-fastmcp-wrap-result"] = True
413
415
  else:
414
416
  output_schema = base_schema
@@ -76,7 +76,7 @@ class ToolManager:
76
76
  except Exception as e:
77
77
  # Skip failed mounts silently, matches existing behavior
78
78
  logger.warning(
79
- f"Failed to get tools from mounted server '{mounted.prefix}': {e}"
79
+ f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
80
80
  )
81
81
  continue
82
82
 
@@ -325,7 +325,8 @@ class TransformedTool(Tool):
325
325
  cls,
326
326
  tool: Tool,
327
327
  name: str | None = None,
328
- description: str | None = None,
328
+ title: str | None | NotSetT = NotSet,
329
+ description: str | None | NotSetT = NotSet,
329
330
  tags: set[str] | None = None,
330
331
  transform_fn: Callable[..., Any] | None = None,
331
332
  transform_args: dict[str, ArgTransform] | None = None,
@@ -342,6 +343,7 @@ class TransformedTool(Tool):
342
343
  to call the parent tool. Functions with **kwargs receive transformed
343
344
  argument names.
344
345
  name: New name for the tool. Defaults to parent tool's name.
346
+ title: New title for the tool. Defaults to parent tool's title.
345
347
  transform_args: Optional transformations for parent tool arguments.
346
348
  Only specified arguments are transformed, others pass through unchanged:
347
349
  - Simple rename (str)
@@ -506,13 +508,18 @@ class TransformedTool(Tool):
506
508
  f"{', '.join(sorted(duplicates))}"
507
509
  )
508
510
 
509
- final_description = description if description is not None else tool.description
511
+ final_name = name or tool.name
512
+ final_description = (
513
+ description if not isinstance(description, NotSetT) else tool.description
514
+ )
515
+ final_title = title if not isinstance(title, NotSetT) else tool.title
510
516
 
511
517
  transformed_tool = cls(
512
518
  fn=final_fn,
513
519
  forwarding_fn=forwarding_fn,
514
520
  parent_tool=tool,
515
- name=name or tool.name,
521
+ name=final_name,
522
+ title=final_title,
516
523
  description=final_description,
517
524
  parameters=final_schema,
518
525
  output_schema=final_output_schema,
fastmcp/utilities/cli.py CHANGED
@@ -14,11 +14,11 @@ if TYPE_CHECKING:
14
14
  from fastmcp import FastMCP
15
15
 
16
16
  LOGO_ASCII = r"""
17
- _ __ ___ ______ __ __ _____________ ____ ____
18
- _ __ ___ / ____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
19
- _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
20
- _ __ ___ / __/ / /_/ (__ ) /_/ / / / /___/ ____/ / __/_/ /_/ /
21
- _ __ ___ /_/ \__,_/____/\__/_/ /_/\____/_/ /_____(_)____/
17
+ _ __ ___ ______ __ __ _____________ ____ ____
18
+ _ __ ___ / ____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
19
+ _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
20
+ _ __ ___ / __/ / /_/ (__ ) /_/ / / / /___/ ____/ / __/_/ /_/ /
21
+ _ __ ___ /_/ \__,_/____/\__/_/ /_/\____/_/ /_____(_)____/
22
22
 
23
23
  """.lstrip("\n")
24
24
 
@@ -94,7 +94,7 @@ def log_server_banner(
94
94
  title="FastMCP 2.0",
95
95
  title_align="left",
96
96
  border_style="dim",
97
- padding=(2, 5),
97
+ padding=(1, 4),
98
98
  expand=False,
99
99
  )
100
100
 
@@ -77,3 +77,46 @@ class FastMCPComponent(FastMCPBaseModel):
77
77
  def disable(self) -> None:
78
78
  """Disable the component."""
79
79
  self.enabled = False
80
+
81
+ def copy(self) -> Self:
82
+ """Create a copy of the component."""
83
+ return self.model_copy()
84
+
85
+
86
+ class MirroredComponent(FastMCPComponent):
87
+ """Base class for components that are mirrored from a remote server.
88
+
89
+ Mirrored components cannot be enabled or disabled directly. Call copy() first
90
+ to create a local version you can modify.
91
+ """
92
+
93
+ _mirrored: bool = PrivateAttr(default=False)
94
+
95
+ def __init__(self, *, _mirrored: bool = False, **kwargs: Any) -> None:
96
+ super().__init__(**kwargs)
97
+ self._mirrored = _mirrored
98
+
99
+ def enable(self) -> None:
100
+ """Enable the component."""
101
+ if self._mirrored:
102
+ raise RuntimeError(
103
+ f"Cannot enable mirrored component '{self.name}'. "
104
+ f"Create a local copy first with {self.name}.copy() and add it to your server."
105
+ )
106
+ super().enable()
107
+
108
+ def disable(self) -> None:
109
+ """Disable the component."""
110
+ if self._mirrored:
111
+ raise RuntimeError(
112
+ f"Cannot disable mirrored component '{self.name}'. "
113
+ f"Create a local copy first with {self.name}.copy() and add it to your server."
114
+ )
115
+ super().disable()
116
+
117
+ def copy(self) -> Self:
118
+ """Create a copy of the component that can be modified."""
119
+ # Create a copy and mark it as not mirrored
120
+ copied = self.model_copy()
121
+ copied._mirrored = False
122
+ return copied
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import logging
3
- from typing import Any, Generic, Literal, TypeVar
3
+ from typing import Any, Generic, Literal, TypeVar, cast
4
4
 
5
5
  from openapi_pydantic import (
6
6
  OpenAPI,
@@ -93,6 +93,40 @@ def format_array_parameter(
93
93
  return str_value
94
94
 
95
95
 
96
+ def format_deep_object_parameter(
97
+ param_value: dict, parameter_name: str
98
+ ) -> dict[str, str]:
99
+ """
100
+ Format a dictionary parameter for deepObject style serialization.
101
+
102
+ According to OpenAPI 3.0 spec, deepObject style with explode=true serializes
103
+ object properties as separate query parameters with bracket notation.
104
+
105
+ For example: {"id": "123", "type": "user"} becomes:
106
+ param[id]=123&param[type]=user
107
+
108
+ Args:
109
+ param_value: Dictionary value to format
110
+ parameter_name: Name of the parameter
111
+
112
+ Returns:
113
+ Dictionary with bracketed parameter names as keys
114
+ """
115
+ if not isinstance(param_value, dict):
116
+ logger.warning(
117
+ f"deepObject style parameter '{parameter_name}' expected dict, got {type(param_value)}"
118
+ )
119
+ return {}
120
+
121
+ result = {}
122
+ for key, value in param_value.items():
123
+ # Format as param[key]=value
124
+ bracketed_key = f"{parameter_name}[{key}]"
125
+ result[bracketed_key] = str(value)
126
+
127
+ return result
128
+
129
+
96
130
  class ParameterInfo(FastMCPBaseModel):
97
131
  """Represents a single parameter for an HTTP operation in our IR."""
98
132
 
@@ -102,6 +136,7 @@ class ParameterInfo(FastMCPBaseModel):
102
136
  schema_: JsonSchema = Field(..., alias="schema") # Target name in IR
103
137
  description: str | None = None
104
138
  explode: bool | None = None # OpenAPI explode property for array parameters
139
+ style: str | None = None # OpenAPI style property for parameter serialization
105
140
 
106
141
 
107
142
  class RequestBodyInfo(FastMCPBaseModel):
@@ -153,6 +188,7 @@ __all__ = [
153
188
  "JsonSchema",
154
189
  "parse_openapi_to_http_routes",
155
190
  "extract_output_schema_from_responses",
191
+ "format_deep_object_parameter",
156
192
  ]
157
193
 
158
194
  # Type variables for generic parser
@@ -415,8 +451,9 @@ class OpenAPIParser(
415
451
  ):
416
452
  param_schema_dict["default"] = resolved_media_schema.default
417
453
 
418
- # Extract explode property if present
454
+ # Extract explode and style properties if present
419
455
  explode = getattr(parameter, "explode", None)
456
+ style = getattr(parameter, "style", None)
420
457
 
421
458
  # Create parameter info object
422
459
  param_info = ParameterInfo(
@@ -426,6 +463,7 @@ class OpenAPIParser(
426
463
  schema=param_schema_dict,
427
464
  description=parameter.description,
428
465
  explode=explode,
466
+ style=style,
429
467
  )
430
468
  extracted_params.append(param_info)
431
469
  except Exception as e:
@@ -1030,15 +1068,16 @@ def _replace_ref_with_defs(
1030
1068
  """
1031
1069
  schema = info.copy()
1032
1070
  if ref_path := schema.get("$ref"):
1033
- if ref_path.startswith("#/components/schemas/"):
1034
- schema_name = ref_path.split("/")[-1]
1035
- schema["$ref"] = f"#/$defs/{schema_name}"
1036
- elif not ref_path.startswith("#/"):
1037
- raise ValueError(
1038
- f"External or non-local reference not supported: {ref_path}. "
1039
- f"FastMCP only supports local schema references starting with '#/'. "
1040
- f"Please include all schema definitions within the OpenAPI document."
1041
- )
1071
+ if isinstance(ref_path, str):
1072
+ if ref_path.startswith("#/components/schemas/"):
1073
+ schema_name = ref_path.split("/")[-1]
1074
+ schema["$ref"] = f"#/$defs/{schema_name}"
1075
+ elif not ref_path.startswith("#/"):
1076
+ raise ValueError(
1077
+ f"External or non-local reference not supported: {ref_path}. "
1078
+ f"FastMCP only supports local schema references starting with '#/'. "
1079
+ f"Please include all schema definitions within the OpenAPI document."
1080
+ )
1042
1081
  elif properties := schema.get("properties"):
1043
1082
  if "$ref" in properties:
1044
1083
  schema["properties"] = _replace_ref_with_defs(properties)
@@ -1057,9 +1096,85 @@ def _replace_ref_with_defs(
1057
1096
  return schema
1058
1097
 
1059
1098
 
1099
+ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:
1100
+ """
1101
+ Make an optional parameter schema nullable to allow None values.
1102
+
1103
+ For optional parameters, we need to allow null values in addition to the
1104
+ specified type to handle cases where None is passed for optional parameters.
1105
+ """
1106
+ # If schema already has multiple types or is already nullable, don't modify
1107
+ if "anyOf" in schema or "oneOf" in schema or "allOf" in schema:
1108
+ return schema
1109
+
1110
+ # If it's already nullable (type includes null), don't modify
1111
+ if isinstance(schema.get("type"), list) and "null" in schema["type"]:
1112
+ return schema
1113
+
1114
+ # Create a new schema that allows null in addition to the original type
1115
+ if "type" in schema:
1116
+ original_type = schema["type"]
1117
+
1118
+ if isinstance(original_type, str):
1119
+ # Single type - make it a union with null
1120
+ nullable_schema = schema.copy()
1121
+
1122
+ nested_non_nullable_schema = {
1123
+ "type": original_type,
1124
+ }
1125
+
1126
+ # If the original type is an array, move the array-specific properties into the now-nested schema
1127
+ # https://json-schema.org/understanding-json-schema/reference/array
1128
+ if original_type == "array":
1129
+ for array_property in [
1130
+ "items",
1131
+ "prefixItems",
1132
+ "unevaluatedItems",
1133
+ "contains",
1134
+ "minContains",
1135
+ "maxContains",
1136
+ "minItems",
1137
+ "maxItems",
1138
+ "uniqueItems",
1139
+ ]:
1140
+ if array_property in nullable_schema:
1141
+ nested_non_nullable_schema[array_property] = nullable_schema[
1142
+ array_property
1143
+ ]
1144
+ del nullable_schema[array_property]
1145
+
1146
+ # If the original type is an object, move the object-specific properties into the now-nested schema
1147
+ # https://json-schema.org/understanding-json-schema/reference/object
1148
+ elif original_type == "object":
1149
+ for object_property in [
1150
+ "properties",
1151
+ "patternProperties",
1152
+ "additionalProperties",
1153
+ "unevaluatedProperties",
1154
+ "required",
1155
+ "propertyNames",
1156
+ "minProperties",
1157
+ "maxProperties",
1158
+ ]:
1159
+ if object_property in nullable_schema:
1160
+ nested_non_nullable_schema[object_property] = nullable_schema[
1161
+ object_property
1162
+ ]
1163
+ del nullable_schema[object_property]
1164
+
1165
+ nullable_schema["anyOf"] = [nested_non_nullable_schema, {"type": "null"}]
1166
+
1167
+ # Remove the original type since we're using anyOf
1168
+ del nullable_schema["type"]
1169
+ return nullable_schema
1170
+
1171
+ return schema
1172
+
1173
+
1060
1174
  def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1061
1175
  """
1062
1176
  Combines parameter and request body schemas into a single schema.
1177
+ Handles parameter name collisions by adding location suffixes.
1063
1178
 
1064
1179
  Args:
1065
1180
  route: HTTPRoute object
@@ -1070,17 +1185,19 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1070
1185
  properties = {}
1071
1186
  required = []
1072
1187
 
1073
- # Add path parameters
1188
+ # First pass: collect parameter names by location and body properties
1189
+ param_names_by_location = {
1190
+ "path": set(),
1191
+ "query": set(),
1192
+ "header": set(),
1193
+ "cookie": set(),
1194
+ }
1195
+ body_props = {}
1196
+
1074
1197
  for param in route.parameters:
1075
- if param.required:
1076
- required.append(param.name)
1077
- properties[param.name] = _replace_ref_with_defs(
1078
- param.schema_.copy(), param.description
1079
- )
1198
+ param_names_by_location[param.location].add(param.name)
1080
1199
 
1081
- # Add request body if it exists
1082
1200
  if route.request_body and route.request_body.content_schema:
1083
- # For now, just use the first content type's schema
1084
1201
  content_type = next(iter(route.request_body.content_schema))
1085
1202
  body_schema = _replace_ref_with_defs(
1086
1203
  route.request_body.content_schema[content_type].copy(),
@@ -1088,7 +1205,54 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1088
1205
  )
1089
1206
  body_props = body_schema.get("properties", {})
1090
1207
 
1091
- # Add request body properties
1208
+ # Detect collisions: parameters that exist in both body and path/query/header
1209
+ all_non_body_params = set()
1210
+ for location_params in param_names_by_location.values():
1211
+ all_non_body_params.update(location_params)
1212
+
1213
+ body_param_names = set(body_props.keys())
1214
+ colliding_params = all_non_body_params & body_param_names
1215
+
1216
+ # Add parameters with suffixes for collisions
1217
+ for param in route.parameters:
1218
+ if param.name in colliding_params:
1219
+ # Add suffix for non-body parameters when collision detected
1220
+ suffixed_name = f"{param.name}__{param.location}"
1221
+ if param.required:
1222
+ required.append(suffixed_name)
1223
+
1224
+ # Add location info to description
1225
+ param_schema = _replace_ref_with_defs(
1226
+ param.schema_.copy(), param.description
1227
+ )
1228
+ original_desc = param_schema.get("description", "")
1229
+ location_desc = f"({param.location.capitalize()} parameter)"
1230
+ if original_desc:
1231
+ param_schema["description"] = f"{original_desc} {location_desc}"
1232
+ else:
1233
+ param_schema["description"] = location_desc
1234
+
1235
+ # Make optional parameters nullable to allow None values
1236
+ if not param.required:
1237
+ param_schema = _make_optional_parameter_nullable(param_schema)
1238
+
1239
+ properties[suffixed_name] = param_schema
1240
+ else:
1241
+ # No collision, use original name
1242
+ if param.required:
1243
+ required.append(param.name)
1244
+ param_schema = _replace_ref_with_defs(
1245
+ param.schema_.copy(), param.description
1246
+ )
1247
+
1248
+ # Make optional parameters nullable to allow None values
1249
+ if not param.required:
1250
+ param_schema = _make_optional_parameter_nullable(param_schema)
1251
+
1252
+ properties[param.name] = param_schema
1253
+
1254
+ # Add request body properties (no suffixes for body parameters)
1255
+ if route.request_body and route.request_body.content_schema:
1092
1256
  for prop_name, prop_schema in body_props.items():
1093
1257
  properties[prop_name] = prop_schema
1094
1258
 
@@ -1110,6 +1274,20 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1110
1274
  return result
1111
1275
 
1112
1276
 
1277
+ def _adjust_union_types(
1278
+ schema: dict[str, Any] | list[Any],
1279
+ ) -> dict[str, Any] | list[Any]:
1280
+ """Recursively replace 'oneOf' with 'anyOf' in schema to handle overlapping unions."""
1281
+ if isinstance(schema, dict):
1282
+ if "oneOf" in schema:
1283
+ schema["anyOf"] = schema.pop("oneOf")
1284
+ for k, v in schema.items():
1285
+ schema[k] = _adjust_union_types(v)
1286
+ elif isinstance(schema, list):
1287
+ return [_adjust_union_types(item) for item in schema]
1288
+ return schema
1289
+
1290
+
1113
1291
  def extract_output_schema_from_responses(
1114
1292
  responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None
1115
1293
  ) -> dict[str, Any] | None:
@@ -1198,4 +1376,7 @@ def extract_output_schema_from_responses(
1198
1376
  # Use compress_schema to remove unused definitions
1199
1377
  output_schema = compress_schema(output_schema)
1200
1378
 
1379
+ # Adjust union types to handle overlapping unions
1380
+ output_schema = cast(dict[str, Any], _adjust_union_types(output_schema))
1381
+
1201
1382
  return output_schema
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.10.4
3
+ Version: 2.10.6
4
4
  Summary: The fast, Pythonic way to build MCP servers and clients.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -38,7 +38,7 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  <strong>The fast, Pythonic way to build MCP servers and clients.</strong>
40
40
 
41
- *FastMCP is made with 💙 by [Prefect](https://www.prefect.io/)*
41
+ *FastMCP is made with ☕️ by [Prefect](https://www.prefect.io/)*
42
42
 
43
43
  [![Docs](https://img.shields.io/badge/docs-gofastmcp.com-blue)](https://gofastmcp.com)
44
44
  [![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp)