fastmcp 2.10.4__py3-none-any.whl → 2.10.5__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.
@@ -1,5 +1,6 @@
1
1
  """Claude Code integration for FastMCP install using Cyclopts."""
2
2
 
3
+ import shutil
3
4
  import subprocess
4
5
  import sys
5
6
  from pathlib import Path
@@ -16,22 +17,50 @@ logger = get_logger(__name__)
16
17
 
17
18
 
18
19
  def find_claude_command() -> str | None:
19
- """Find the Claude Code CLI command."""
20
- # Check the default installation location
21
- default_path = Path.home() / ".claude" / "local" / "claude"
22
- if default_path.exists():
20
+ """Find the Claude Code CLI command.
21
+
22
+ Checks common installation locations since 'claude' is often a shell alias
23
+ that doesn't work with subprocess calls.
24
+ """
25
+ # First try shutil.which() in case it's a real executable in PATH
26
+ claude_in_path = shutil.which("claude")
27
+ if claude_in_path:
23
28
  try:
24
29
  result = subprocess.run(
25
- [str(default_path), "--version"],
30
+ [claude_in_path, "--version"],
26
31
  check=True,
27
32
  capture_output=True,
28
33
  text=True,
29
34
  )
30
35
  if "Claude Code" in result.stdout:
31
- return str(default_path)
36
+ return claude_in_path
32
37
  except (subprocess.CalledProcessError, FileNotFoundError):
33
38
  pass
34
39
 
40
+ # Check common installation locations (aliases don't work with subprocess)
41
+ potential_paths = [
42
+ # Default Claude Code installation location (after migration)
43
+ Path.home() / ".claude" / "local" / "claude",
44
+ # npm global installation on macOS/Linux (default)
45
+ Path("/usr/local/bin/claude"),
46
+ # npm global installation with custom prefix
47
+ Path.home() / ".npm-global" / "bin" / "claude",
48
+ ]
49
+
50
+ for path in potential_paths:
51
+ if path.exists():
52
+ try:
53
+ result = subprocess.run(
54
+ [str(path), "--version"],
55
+ check=True,
56
+ capture_output=True,
57
+ text=True,
58
+ )
59
+ if "Claude Code" in result.stdout:
60
+ return str(path)
61
+ except (subprocess.CalledProcessError, FileNotFoundError):
62
+ continue
63
+
35
64
  return None
36
65
 
37
66
 
@@ -2,18 +2,10 @@ from .middleware import (
2
2
  Middleware,
3
3
  MiddlewareContext,
4
4
  CallNext,
5
- ListToolsResult,
6
- ListResourcesResult,
7
- ListResourceTemplatesResult,
8
- ListPromptsResult,
9
5
  )
10
6
 
11
7
  __all__ = [
12
8
  "Middleware",
13
9
  "MiddlewareContext",
14
10
  "CallNext",
15
- "ListToolsResult",
16
- "ListResourcesResult",
17
- "ListResourceTemplatesResult",
18
- "ListPromptsResult",
19
11
  ]
@@ -29,10 +29,6 @@ __all__ = [
29
29
  "Middleware",
30
30
  "MiddlewareContext",
31
31
  "CallNext",
32
- "ListToolsResult",
33
- "ListResourcesResult",
34
- "ListResourceTemplatesResult",
35
- "ListPromptsResult",
36
32
  ]
37
33
 
38
34
  logger = logging.getLogger(__name__)
@@ -62,26 +58,6 @@ ServerResultT = TypeVar(
62
58
  )
63
59
 
64
60
 
65
- @dataclass(kw_only=True)
66
- class ListToolsResult:
67
- tools: dict[str, Tool]
68
-
69
-
70
- @dataclass(kw_only=True)
71
- class ListResourcesResult:
72
- resources: list[Resource]
73
-
74
-
75
- @dataclass(kw_only=True)
76
- class ListResourceTemplatesResult:
77
- resource_templates: list[ResourceTemplate]
78
-
79
-
80
- @dataclass(kw_only=True)
81
- class ListPromptsResult:
82
- prompts: list[Prompt]
83
-
84
-
85
61
  @runtime_checkable
86
62
  class ServerResultProtocol(Protocol[ServerResultT]):
87
63
  root: ServerResultT
@@ -212,29 +188,27 @@ class Middleware:
212
188
  async def on_list_tools(
213
189
  self,
214
190
  context: MiddlewareContext[mt.ListToolsRequest],
215
- call_next: CallNext[mt.ListToolsRequest, ListToolsResult],
216
- ) -> ListToolsResult:
191
+ call_next: CallNext[mt.ListToolsRequest, list[Tool]],
192
+ ) -> list[Tool]:
217
193
  return await call_next(context)
218
194
 
219
195
  async def on_list_resources(
220
196
  self,
221
197
  context: MiddlewareContext[mt.ListResourcesRequest],
222
- call_next: CallNext[mt.ListResourcesRequest, ListResourcesResult],
223
- ) -> ListResourcesResult:
198
+ call_next: CallNext[mt.ListResourcesRequest, list[Resource]],
199
+ ) -> list[Resource]:
224
200
  return await call_next(context)
225
201
 
226
202
  async def on_list_resource_templates(
227
203
  self,
228
204
  context: MiddlewareContext[mt.ListResourceTemplatesRequest],
229
- call_next: CallNext[
230
- mt.ListResourceTemplatesRequest, ListResourceTemplatesResult
231
- ],
232
- ) -> ListResourceTemplatesResult:
205
+ call_next: CallNext[mt.ListResourceTemplatesRequest, list[ResourceTemplate]],
206
+ ) -> list[ResourceTemplate]:
233
207
  return await call_next(context)
234
208
 
235
209
  async def on_list_prompts(
236
210
  self,
237
211
  context: MiddlewareContext[mt.ListPromptsRequest],
238
- call_next: CallNext[mt.ListPromptsRequest, ListPromptsResult],
239
- ) -> ListPromptsResult:
212
+ call_next: CallNext[mt.ListPromptsRequest, list[Prompt]],
213
+ ) -> list[Prompt]:
240
214
  return await call_next(context)
fastmcp/server/openapi.py CHANGED
@@ -29,6 +29,7 @@ from fastmcp.utilities.openapi import (
29
29
  _combine_schemas,
30
30
  extract_output_schema_from_responses,
31
31
  format_array_parameter,
32
+ format_deep_object_parameter,
32
33
  format_description_with_responses,
33
34
  )
34
35
 
@@ -261,19 +262,45 @@ class OpenAPITool(Tool):
261
262
  async def run(self, arguments: dict[str, Any]) -> ToolResult:
262
263
  """Execute the HTTP request based on the route configuration."""
263
264
 
265
+ # Create mapping from suffixed parameter names back to original names and locations
266
+ # This handles parameter collisions where suffixes were added during schema generation
267
+ param_mapping = {} # suffixed_name -> (original_name, location)
268
+
269
+ # First, check if we have request body properties to detect collisions
270
+ body_props = set()
271
+ if self._route.request_body and self._route.request_body.content_schema:
272
+ content_type = next(iter(self._route.request_body.content_schema))
273
+ body_schema = self._route.request_body.content_schema[content_type]
274
+ body_props = set(body_schema.get("properties", {}).keys())
275
+
276
+ # Build parameter mapping for potentially suffixed parameters
277
+ for param in self._route.parameters:
278
+ original_name = param.name
279
+ suffixed_name = f"{param.name}__{param.location}"
280
+
281
+ # If parameter name collides with body property, it would have been suffixed
282
+ if param.name in body_props:
283
+ param_mapping[suffixed_name] = (original_name, param.location)
284
+ # Also map original name for backward compatibility when no collision
285
+ param_mapping[original_name] = (original_name, param.location)
286
+
264
287
  # Prepare URL
265
288
  path = self._route.path
266
289
 
267
- # Replace path parameters with values from kwargs
268
- # Path parameters should never be None as they're typically required
269
- # but we'll handle that case anyway
270
- path_params = {
271
- p.name: arguments.get(p.name)
272
- for p in self._route.parameters
273
- if p.location == "path"
274
- and p.name in arguments
275
- and arguments.get(p.name) is not None
276
- }
290
+ # Replace path parameters with values from arguments
291
+ # Look for both original and suffixed parameter names
292
+ path_params = {}
293
+ for p in self._route.parameters:
294
+ if p.location == "path":
295
+ # Try suffixed name first, then original name
296
+ suffixed_name = f"{p.name}__{p.location}"
297
+ if (
298
+ suffixed_name in arguments
299
+ and arguments.get(suffixed_name) is not None
300
+ ):
301
+ path_params[p.name] = arguments[suffixed_name]
302
+ elif p.name in arguments and arguments.get(p.name) is not None:
303
+ path_params[p.name] = arguments[p.name]
277
304
 
278
305
  # Ensure all path parameters are provided
279
306
  required_path_params = {
@@ -312,35 +339,67 @@ class OpenAPITool(Tool):
312
339
  # Prepare query parameters - filter out None and empty strings
313
340
  query_params = {}
314
341
  for p in self._route.parameters:
315
- if (
316
- p.location == "query"
317
- and p.name in arguments
318
- and arguments.get(p.name) is not None
319
- and arguments.get(p.name) != ""
320
- ):
321
- param_value = arguments.get(p.name)
322
-
323
- # Format array query parameters as comma-separated strings
324
- # following OpenAPI form style (default for query parameters)
325
- if isinstance(param_value, list) and p.schema_.get("type") == "array":
326
- # Get explode parameter from the parameter info, default is True for query parameters
327
- # If explode is True, the array is serialized as separate parameters
328
- # If explode is False, the array is serialized as a comma-separated string
329
- explode = p.explode if p.explode is not None else True
330
-
331
- if explode:
332
- # When explode=True, we pass the array directly, which HTTPX will serialize
333
- # as multiple parameters with the same name
334
- query_params[p.name] = param_value
342
+ if p.location == "query":
343
+ # Try suffixed name first, then original name
344
+ suffixed_name = f"{p.name}__{p.location}"
345
+ param_value = None
346
+
347
+ if (
348
+ suffixed_name in arguments
349
+ and arguments.get(suffixed_name) is not None
350
+ and arguments.get(suffixed_name) != ""
351
+ ):
352
+ param_value = arguments[suffixed_name]
353
+ elif (
354
+ p.name in arguments
355
+ and arguments.get(p.name) is not None
356
+ and arguments.get(p.name) != ""
357
+ ):
358
+ param_value = arguments[p.name]
359
+
360
+ if param_value is not None:
361
+ # Handle different parameter styles and types
362
+ param_style = (
363
+ p.style or "form"
364
+ ) # Default style for query parameters is "form"
365
+ param_explode = (
366
+ p.explode if p.explode is not None else True
367
+ ) # Default explode for query is True
368
+
369
+ # Handle deepObject style for object parameters
370
+ if param_style == "deepObject" and isinstance(param_value, dict):
371
+ if param_explode:
372
+ # deepObject with explode=true: object properties become separate parameters
373
+ # e.g., target[id]=123&target[type]=user
374
+ deep_obj_params = format_deep_object_parameter(
375
+ param_value, p.name
376
+ )
377
+ query_params.update(deep_obj_params)
378
+ else:
379
+ # deepObject with explode=false is not commonly used, fallback to JSON
380
+ logger.warning(
381
+ f"deepObject style with explode=false for parameter '{p.name}' is not standard. "
382
+ f"Using JSON serialization fallback."
383
+ )
384
+ query_params[p.name] = json.dumps(param_value)
385
+ # Handle array parameters with form style (default)
386
+ elif (
387
+ isinstance(param_value, list)
388
+ and p.schema_.get("type") == "array"
389
+ ):
390
+ if param_explode:
391
+ # When explode=True, we pass the array directly, which HTTPX will serialize
392
+ # as multiple parameters with the same name
393
+ query_params[p.name] = param_value
394
+ else:
395
+ # Format array as comma-separated string when explode=False
396
+ formatted_value = format_array_parameter(
397
+ param_value, p.name, is_query_parameter=True
398
+ )
399
+ query_params[p.name] = formatted_value
335
400
  else:
336
- # Format array as comma-separated string when explode=False
337
- formatted_value = format_array_parameter(
338
- param_value, p.name, is_query_parameter=True
339
- )
340
- query_params[p.name] = formatted_value
341
- else:
342
- # Non-array parameters are passed as is
343
- query_params[p.name] = param_value
401
+ # Non-array, non-deepObject parameters are passed as is
402
+ query_params[p.name] = param_value
344
403
 
345
404
  # Prepare headers - fix typing by ensuring all values are strings
346
405
  headers = {}
@@ -348,12 +407,21 @@ class OpenAPITool(Tool):
348
407
  # Start with OpenAPI-defined header parameters
349
408
  openapi_headers = {}
350
409
  for p in self._route.parameters:
351
- if (
352
- p.location == "header"
353
- and p.name in arguments
354
- and arguments[p.name] is not None
355
- ):
356
- openapi_headers[p.name.lower()] = str(arguments[p.name])
410
+ if p.location == "header":
411
+ # Try suffixed name first, then original name
412
+ suffixed_name = f"{p.name}__{p.location}"
413
+ param_value = None
414
+
415
+ if (
416
+ suffixed_name in arguments
417
+ and arguments.get(suffixed_name) is not None
418
+ ):
419
+ param_value = arguments[suffixed_name]
420
+ elif p.name in arguments and arguments.get(p.name) is not None:
421
+ param_value = arguments[p.name]
422
+
423
+ if param_value is not None:
424
+ openapi_headers[p.name.lower()] = str(param_value)
357
425
  headers.update(openapi_headers)
358
426
 
359
427
  # Add headers from the current MCP client HTTP request (these take precedence)
@@ -363,16 +431,22 @@ class OpenAPITool(Tool):
363
431
  # Prepare request body
364
432
  json_data = None
365
433
  if self._route.request_body and self._route.request_body.content_schema:
366
- # Extract body parameters, excluding path/query/header params that were already used
367
- path_query_header_params = {
368
- p.name
369
- for p in self._route.parameters
370
- if p.location in ("path", "query", "header")
371
- }
434
+ # Extract body parameters with collision-aware logic
435
+ # Exclude all parameter names that belong to path/query/header locations
436
+ params_to_exclude = set()
437
+
438
+ for p in self._route.parameters:
439
+ if (
440
+ p.name in body_props
441
+ ): # This parameter had a collision, so it was suffixed
442
+ params_to_exclude.add(f"{p.name}__{p.location}")
443
+ else: # No collision, parameter keeps original name but should still be excluded from body
444
+ params_to_exclude.add(p.name)
445
+
372
446
  body_params = {
373
447
  k: v
374
448
  for k, v in arguments.items()
375
- if k not in path_query_header_params and k != "context"
449
+ if k not in params_to_exclude and k != "context"
376
450
  }
377
451
 
378
452
  if body_params:
fastmcp/server/proxy.py CHANGED
@@ -36,6 +36,7 @@ from fastmcp.server.dependencies import get_context
36
36
  from fastmcp.server.server import FastMCP
37
37
  from fastmcp.tools.tool import Tool, ToolResult
38
38
  from fastmcp.tools.tool_manager import ToolManager
39
+ from fastmcp.utilities.components import MirroredComponent
39
40
  from fastmcp.utilities.logging import get_logger
40
41
 
41
42
  if TYPE_CHECKING:
@@ -226,7 +227,7 @@ class ProxyPromptManager(PromptManager):
226
227
  return result
227
228
 
228
229
 
229
- class ProxyTool(Tool):
230
+ class ProxyTool(Tool, MirroredComponent):
230
231
  """
231
232
  A Tool that represents and executes a tool on a remote server.
232
233
  """
@@ -245,6 +246,7 @@ class ProxyTool(Tool):
245
246
  parameters=mcp_tool.inputSchema,
246
247
  annotations=mcp_tool.annotations,
247
248
  output_schema=mcp_tool.outputSchema,
249
+ _mirrored=True,
248
250
  )
249
251
 
250
252
  async def run(
@@ -266,7 +268,7 @@ class ProxyTool(Tool):
266
268
  )
267
269
 
268
270
 
269
- class ProxyResource(Resource):
271
+ class ProxyResource(Resource, MirroredComponent):
270
272
  """
271
273
  A Resource that represents and reads a resource from a remote server.
272
274
  """
@@ -298,6 +300,7 @@ class ProxyResource(Resource):
298
300
  name=mcp_resource.name,
299
301
  description=mcp_resource.description,
300
302
  mime_type=mcp_resource.mimeType or "text/plain",
303
+ _mirrored=True,
301
304
  )
302
305
 
303
306
  async def read(self) -> str | bytes:
@@ -315,7 +318,7 @@ class ProxyResource(Resource):
315
318
  raise ResourceError(f"Unsupported content type: {type(result[0])}")
316
319
 
317
320
 
318
- class ProxyTemplate(ResourceTemplate):
321
+ class ProxyTemplate(ResourceTemplate, MirroredComponent):
319
322
  """
320
323
  A ResourceTemplate that represents and creates resources from a remote server template.
321
324
  """
@@ -336,6 +339,7 @@ class ProxyTemplate(ResourceTemplate):
336
339
  description=mcp_template.description,
337
340
  mime_type=mcp_template.mimeType or "text/plain",
338
341
  parameters={}, # Remote templates don't have local parameters
342
+ _mirrored=True,
339
343
  )
340
344
 
341
345
  async def create_resource(
@@ -371,7 +375,7 @@ class ProxyTemplate(ResourceTemplate):
371
375
  )
372
376
 
373
377
 
374
- class ProxyPrompt(Prompt):
378
+ class ProxyPrompt(Prompt, MirroredComponent):
375
379
  """
376
380
  A Prompt that represents and renders a prompt from a remote server.
377
381
  """
@@ -400,6 +404,7 @@ class ProxyPrompt(Prompt):
400
404
  name=mcp_prompt.name,
401
405
  description=mcp_prompt.description,
402
406
  arguments=arguments,
407
+ _mirrored=True,
403
408
  )
404
409
 
405
410
  async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
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,
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:
@@ -1060,6 +1098,7 @@ def _replace_ref_with_defs(
1060
1098
  def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1061
1099
  """
1062
1100
  Combines parameter and request body schemas into a single schema.
1101
+ Handles parameter name collisions by adding location suffixes.
1063
1102
 
1064
1103
  Args:
1065
1104
  route: HTTPRoute object
@@ -1070,17 +1109,19 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1070
1109
  properties = {}
1071
1110
  required = []
1072
1111
 
1073
- # Add path parameters
1112
+ # First pass: collect parameter names by location and body properties
1113
+ param_names_by_location = {
1114
+ "path": set(),
1115
+ "query": set(),
1116
+ "header": set(),
1117
+ "cookie": set(),
1118
+ }
1119
+ body_props = {}
1120
+
1074
1121
  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
- )
1122
+ param_names_by_location[param.location].add(param.name)
1080
1123
 
1081
- # Add request body if it exists
1082
1124
  if route.request_body and route.request_body.content_schema:
1083
- # For now, just use the first content type's schema
1084
1125
  content_type = next(iter(route.request_body.content_schema))
1085
1126
  body_schema = _replace_ref_with_defs(
1086
1127
  route.request_body.content_schema[content_type].copy(),
@@ -1088,7 +1129,44 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1088
1129
  )
1089
1130
  body_props = body_schema.get("properties", {})
1090
1131
 
1091
- # Add request body properties
1132
+ # Detect collisions: parameters that exist in both body and path/query/header
1133
+ all_non_body_params = set()
1134
+ for location_params in param_names_by_location.values():
1135
+ all_non_body_params.update(location_params)
1136
+
1137
+ body_param_names = set(body_props.keys())
1138
+ colliding_params = all_non_body_params & body_param_names
1139
+
1140
+ # Add parameters with suffixes for collisions
1141
+ for param in route.parameters:
1142
+ if param.name in colliding_params:
1143
+ # Add suffix for non-body parameters when collision detected
1144
+ suffixed_name = f"{param.name}__{param.location}"
1145
+ if param.required:
1146
+ required.append(suffixed_name)
1147
+
1148
+ # Add location info to description
1149
+ param_schema = _replace_ref_with_defs(
1150
+ param.schema_.copy(), param.description
1151
+ )
1152
+ original_desc = param_schema.get("description", "")
1153
+ location_desc = f"({param.location.capitalize()} parameter)"
1154
+ if original_desc:
1155
+ param_schema["description"] = f"{original_desc} {location_desc}"
1156
+ else:
1157
+ param_schema["description"] = location_desc
1158
+
1159
+ properties[suffixed_name] = param_schema
1160
+ else:
1161
+ # No collision, use original name
1162
+ if param.required:
1163
+ required.append(param.name)
1164
+ properties[param.name] = _replace_ref_with_defs(
1165
+ param.schema_.copy(), param.description
1166
+ )
1167
+
1168
+ # Add request body properties (no suffixes for body parameters)
1169
+ if route.request_body and route.request_body.content_schema:
1092
1170
  for prop_name, prop_schema in body_props.items():
1093
1171
  properties[prop_name] = prop_schema
1094
1172
 
@@ -1110,6 +1188,20 @@ def _combine_schemas(route: HTTPRoute) -> dict[str, Any]:
1110
1188
  return result
1111
1189
 
1112
1190
 
1191
+ def _adjust_union_types(
1192
+ schema: dict[str, Any] | list[Any],
1193
+ ) -> dict[str, Any] | list[Any]:
1194
+ """Recursively replace 'oneOf' with 'anyOf' in schema to handle overlapping unions."""
1195
+ if isinstance(schema, dict):
1196
+ if "oneOf" in schema:
1197
+ schema["anyOf"] = schema.pop("oneOf")
1198
+ for k, v in schema.items():
1199
+ schema[k] = _adjust_union_types(v)
1200
+ elif isinstance(schema, list):
1201
+ return [_adjust_union_types(item) for item in schema]
1202
+ return schema
1203
+
1204
+
1113
1205
  def extract_output_schema_from_responses(
1114
1206
  responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None
1115
1207
  ) -> dict[str, Any] | None:
@@ -1198,4 +1290,7 @@ def extract_output_schema_from_responses(
1198
1290
  # Use compress_schema to remove unused definitions
1199
1291
  output_schema = compress_schema(output_schema)
1200
1292
 
1293
+ # Adjust union types to handle overlapping unions
1294
+ output_schema = cast(dict[str, Any], _adjust_union_types(output_schema))
1295
+
1201
1296
  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.5
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
@@ -8,7 +8,7 @@ fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
8
8
  fastmcp/cli/cli.py,sha256=Q4HDVDSty1Gx6qN_M4FDx8RYat34vhWT25338X-quNs,12672
9
9
  fastmcp/cli/run.py,sha256=V268Lf7LXdeMZ4_D4fKdFST7cOs8pGgLXZTxtcEJRWg,6715
10
10
  fastmcp/cli/install/__init__.py,sha256=afaNANIhivfKi4QhJdYUP56Q2XZ4da4dUbDq9HAMJqo,701
11
- fastmcp/cli/install/claude_code.py,sha256=eH6Pdre-FtzGuEatWYIFVTNtEEjHb7m-bgX4ARIFG2g,5384
11
+ fastmcp/cli/install/claude_code.py,sha256=VlFVGKKRppkmp42io6VPTQrQHgNww4H2ppa6mAWM-Ao,6430
12
12
  fastmcp/cli/install/claude_desktop.py,sha256=o3p3KiW0QzsrzOEa3OU_gkvzTuFSkabDwEqwwgp1mYU,5590
13
13
  fastmcp/cli/install/cursor.py,sha256=296a22T3fdaC8deCwh3TreVFoJ0QpjLxRF1gNsp3Wjg,5555
14
14
  fastmcp/cli/install/mcp_config.py,sha256=D1vkZj13ofNFbFhdn8eAuEx-CHTr3nGiVkjSZTdnetY,4559
@@ -54,19 +54,19 @@ fastmcp/server/dependencies.py,sha256=iKJdz1XsVJcrfHo_reXj9ZSldw-HeAwsp9S6lAgfGA
54
54
  fastmcp/server/elicitation.py,sha256=jZIHjV4NjhYbT-w8pBArwd0vNzP8OYwzmsnWDdk6Bd0,6136
55
55
  fastmcp/server/http.py,sha256=d0Jij4HVTaAohluRXArSniXLb1HcHP3ytbe-mMHg6nE,11678
56
56
  fastmcp/server/low_level.py,sha256=LNmc_nU_wx-fRG8OEHdLPKopZpovcrWlyAxJzKss3TA,1239
57
- fastmcp/server/openapi.py,sha256=YoARNzEZQs6MfnGfl5-V9kULCDp1LluJL2yeH9m62Os,37595
58
- fastmcp/server/proxy.py,sha256=YxlU4lNPGb5rk4oyUySYiOMz3GIAywrQP1rMcVYSih4,21689
59
- fastmcp/server/server.py,sha256=Zk77BcHyr0lSwf2LmNOQXiDJNFeQKkZ_p20dg81P0IU,82383
57
+ fastmcp/server/openapi.py,sha256=LWT5rI8TN90MCppuo-LWkYM_ZVnxT6xtS-7Ny8sJIFI,41531
58
+ fastmcp/server/proxy.py,sha256=3cABSJyOalxnqmHGaS8kb6jyaJEAXshQcOh8XihY7Kk,21936
59
+ fastmcp/server/server.py,sha256=8ah5ZYgKLFT2sHzPRT3NUKBrQWi4nny_LPx6cN8EhQs,82816
60
60
  fastmcp/server/auth/__init__.py,sha256=doHCLwOIElvH1NrTdpeP9JKfnNf3MDYPSpQfdsQ-uI0,84
61
61
  fastmcp/server/auth/auth.py,sha256=A00OKxglEMrGMMIiMbc6UmpGc2VoWDkEVU5g2pIzDIg,2119
62
62
  fastmcp/server/auth/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
63
  fastmcp/server/auth/providers/bearer.py,sha256=rjwuKFgCm4fLsAQzUaIaE8OD6bml3sBaO7NYq3Wgs24,16718
64
64
  fastmcp/server/auth/providers/bearer_env.py,sha256=NYPCW363Q8u8BdiPPz1FdB3_kwmbCaWT5yKdAO-ZgwA,2081
65
65
  fastmcp/server/auth/providers/in_memory.py,sha256=Sb3GOtLL2bWbm8z-T8cEsMz1qcQUSHpPEEgYRvTOQi4,14251
66
- fastmcp/server/middleware/__init__.py,sha256=m1QJFQ7JW_2JHpJp1FurBNYaxbBUa_HyDn1Bw9mtyvc,367
66
+ fastmcp/server/middleware/__init__.py,sha256=vh5C9ubN6q-y5QND32P4mQ4zDT89C7XYK39yqhELNAk,155
67
67
  fastmcp/server/middleware/error_handling.py,sha256=SoDatr9i3T2qSIUbSEGWrOnu4WPPyMDymnsF5GR_BiE,7572
68
68
  fastmcp/server/middleware/logging.py,sha256=UIAoafnKRGWpQa7OX5nzChep-9EhKdyTDBmmcRcEVdo,6239
69
- fastmcp/server/middleware/middleware.py,sha256=1Zy5KdBCrGzqFIpYZqWlZq9rLK3NDlRiXhJ2j-YGKeQ,6961
69
+ fastmcp/server/middleware/middleware.py,sha256=jFy7NmLHvGmrGkKViS1PvR_7oGM1EklxNx66EJtUzYU,6441
70
70
  fastmcp/server/middleware/rate_limiting.py,sha256=VTrCoQFmWCm0BxwOrNfG21CBFDDOKJT7IiSEjpJgmPA,7921
71
71
  fastmcp/server/middleware/timing.py,sha256=lL_xc-ErLD5lplfvd5-HIyWEbZhgNBYkcQ74KFXAMkA,5591
72
72
  fastmcp/tools/__init__.py,sha256=vzqb-Y7Kf0d5T0aOsld-O-FA8kD7-4uFExChewFHEzY,201
@@ -75,19 +75,19 @@ fastmcp/tools/tool_manager.py,sha256=Sm_tOO-SY0m7tEN_dofP-tvBnC2HroPRKLU6sp8gnUw
75
75
  fastmcp/tools/tool_transform.py,sha256=nGvxxKsyfp3gYl_nkYFHlnb8Fc4Jtw6t7WL291S4Vh4,32558
76
76
  fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
77
77
  fastmcp/utilities/cache.py,sha256=aV3oZ-ZhMgLSM9iAotlUlEy5jFvGXrVo0Y5Bj4PBtqY,707
78
- fastmcp/utilities/cli.py,sha256=n3HA8IXg7hKaizuM3SRCW65UMXlqjUDf5USRSuscTdY,3287
79
- fastmcp/utilities/components.py,sha256=WIxNVZ7YxCLpdIm_pbTYeP0lAxikvgptVYhIL0LVmCc,2535
78
+ fastmcp/utilities/cli.py,sha256=TXuSyALFAGJwi7tWEBwBmaGhYZBdF1aG6dLgl3zjM1w,3272
79
+ fastmcp/utilities/components.py,sha256=0eldO8mqG0o0sLlMK5heTOFaQdLzscn1Mup-AR-MnTA,3994
80
80
  fastmcp/utilities/exceptions.py,sha256=7Z9j5IzM5rT27BC1Mcn8tkS-bjqCYqMKwb2MMTaxJYU,1350
81
81
  fastmcp/utilities/http.py,sha256=1ns1ymBS-WSxbZjGP6JYjSO52Wa_ls4j4WbnXiupoa4,245
82
82
  fastmcp/utilities/inspect.py,sha256=XNA0dfYM5G-FVbJaVJO8loSUUCNypyLA-QjqTOneJyU,10833
83
83
  fastmcp/utilities/json_schema.py,sha256=8qyb3qOvk1gc3p63uP6LGyKdOANkNdD9YA32OiBxyNw,5495
84
84
  fastmcp/utilities/json_schema_type.py,sha256=Sml03nJGOnUfxCGrHWRMwZMultV0X5JThMepUnHIUiA,22377
85
85
  fastmcp/utilities/logging.py,sha256=1y7oNmy8WrR0NsfNVw1LPoKu92OFdmzIO65syOKi_BI,1388
86
- fastmcp/utilities/openapi.py,sha256=YWDFjT9ng4xi_kzNCoeBCgwnlep14IQ7PnCSQt_lVyU,48528
86
+ fastmcp/utilities/openapi.py,sha256=Fvn6M_deDdm3qHGMLXClpIi5ZBp9bM31lvZ9XSnQkH4,52047
87
87
  fastmcp/utilities/tests.py,sha256=kZH8HQAC702a5vNJb4K0tO1ll9CZADWQ_P-5ERWSvSA,4242
88
88
  fastmcp/utilities/types.py,sha256=c6HPvHCpkq8EXh0hWjaUlj9aCZklmxzAQHCXZy7llNo,10636
89
- fastmcp-2.10.4.dist-info/METADATA,sha256=tZZouDJy7k6EATdwFr8pT48kjmGbiQgl_flsmB1Z3sE,17842
90
- fastmcp-2.10.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
- fastmcp-2.10.4.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
92
- fastmcp-2.10.4.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
93
- fastmcp-2.10.4.dist-info/RECORD,,
89
+ fastmcp-2.10.5.dist-info/METADATA,sha256=T3PxSdKmyz51Fpby151d0mvdIQX-HZK0ns0aXlyIql0,17842
90
+ fastmcp-2.10.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
+ fastmcp-2.10.5.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
92
+ fastmcp-2.10.5.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
93
+ fastmcp-2.10.5.dist-info/RECORD,,