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/__init__.py +7 -2
- fastmcp/cli/install/__init__.py +2 -2
- fastmcp/cli/install/claude_code.py +35 -6
- fastmcp/cli/install/{mcp_config.py → mcp_json.py} +10 -7
- fastmcp/prompts/prompt_manager.py +1 -1
- fastmcp/resources/resource_manager.py +2 -2
- fastmcp/server/context.py +3 -0
- fastmcp/server/middleware/__init__.py +0 -8
- fastmcp/server/middleware/middleware.py +8 -34
- fastmcp/server/openapi.py +140 -53
- fastmcp/server/proxy.py +51 -4
- fastmcp/server/server.py +26 -8
- fastmcp/settings.py +10 -12
- fastmcp/tools/tool.py +5 -3
- fastmcp/tools/tool_manager.py +1 -1
- fastmcp/tools/tool_transform.py +10 -3
- fastmcp/utilities/cli.py +6 -6
- fastmcp/utilities/components.py +43 -0
- fastmcp/utilities/openapi.py +201 -20
- {fastmcp-2.10.4.dist-info → fastmcp-2.10.6.dist-info}/METADATA +2 -2
- {fastmcp-2.10.4.dist-info → fastmcp-2.10.6.dist-info}/RECORD +24 -24
- {fastmcp-2.10.4.dist-info → fastmcp-2.10.6.dist-info}/WHEEL +0 -0
- {fastmcp-2.10.4.dist-info → fastmcp-2.10.6.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.10.4.dist-info → fastmcp-2.10.6.dist-info}/licenses/LICENSE +0 -0
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) ->
|
|
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) ->
|
|
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
|
-
|
|
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) ->
|
|
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.
|
|
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 =
|
|
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,
|
|
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 =
|
|
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
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -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
|
|
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
|
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -325,7 +325,8 @@ class TransformedTool(Tool):
|
|
|
325
325
|
cls,
|
|
326
326
|
tool: Tool,
|
|
327
327
|
name: str | None = None,
|
|
328
|
-
|
|
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
|
-
|
|
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=
|
|
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=(
|
|
97
|
+
padding=(1, 4),
|
|
98
98
|
expand=False,
|
|
99
99
|
)
|
|
100
100
|
|
fastmcp/utilities/components.py
CHANGED
|
@@ -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
|
fastmcp/utilities/openapi.py
CHANGED
|
@@ -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¶m[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
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
41
|
+
*FastMCP is made with ☕️ by [Prefect](https://www.prefect.io/)*
|
|
42
42
|
|
|
43
43
|
[](https://gofastmcp.com)
|
|
44
44
|
[](https://pypi.org/project/fastmcp)
|