fastmcp 2.12.5__py3-none-any.whl → 2.13.2__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 (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -97,11 +97,11 @@ def make_endpoint(action, component, config):
97
97
  return JSONResponse(
98
98
  {"message": f"{action.capitalize()}d {component}: {name}"}
99
99
  )
100
- except NotFoundError:
100
+ except NotFoundError as e:
101
101
  raise StarletteHTTPException(
102
102
  status_code=404,
103
103
  detail=f"Unknown {component}: {name}",
104
- )
104
+ ) from e
105
105
 
106
106
  return endpoint
107
107
 
@@ -41,7 +41,7 @@ class ComponentService:
41
41
  return tool
42
42
 
43
43
  # 2. Check mounted servers using the filtered protocol path.
44
- for mounted in reversed(self._tool_manager._mounted_servers):
44
+ for mounted in reversed(self._server._mounted_servers):
45
45
  if mounted.prefix:
46
46
  if key.startswith(f"{mounted.prefix}_"):
47
47
  tool_key = key.removeprefix(f"{mounted.prefix}_")
@@ -70,7 +70,7 @@ class ComponentService:
70
70
  return tool
71
71
 
72
72
  # 2. Check mounted servers using the filtered protocol path.
73
- for mounted in reversed(self._tool_manager._mounted_servers):
73
+ for mounted in reversed(self._server._mounted_servers):
74
74
  if mounted.prefix:
75
75
  if key.startswith(f"{mounted.prefix}_"):
76
76
  tool_key = key.removeprefix(f"{mounted.prefix}_")
@@ -103,7 +103,7 @@ class ComponentService:
103
103
  return template
104
104
 
105
105
  # 2. Check mounted servers using the filtered protocol path.
106
- for mounted in reversed(self._resource_manager._mounted_servers):
106
+ for mounted in reversed(self._server._mounted_servers):
107
107
  if mounted.prefix:
108
108
  if has_resource_prefix(
109
109
  key,
@@ -146,7 +146,7 @@ class ComponentService:
146
146
  return template
147
147
 
148
148
  # 2. Check mounted servers using the filtered protocol path.
149
- for mounted in reversed(self._resource_manager._mounted_servers):
149
+ for mounted in reversed(self._server._mounted_servers):
150
150
  if mounted.prefix:
151
151
  if has_resource_prefix(
152
152
  key,
@@ -185,7 +185,7 @@ class ComponentService:
185
185
  return prompt
186
186
 
187
187
  # 2. Check mounted servers using the filtered protocol path.
188
- for mounted in reversed(self._prompt_manager._mounted_servers):
188
+ for mounted in reversed(self._server._mounted_servers):
189
189
  if mounted.prefix:
190
190
  if key.startswith(f"{mounted.prefix}_"):
191
191
  prompt_key = key.removeprefix(f"{mounted.prefix}_")
@@ -213,7 +213,7 @@ class ComponentService:
213
213
  return prompt
214
214
 
215
215
  # 2. Check mounted servers using the filtered protocol path.
216
- for mounted in reversed(self._prompt_manager._mounted_servers):
216
+ for mounted in reversed(self._server._mounted_servers):
217
217
  if mounted.prefix:
218
218
  if key.startswith(f"{mounted.prefix}_"):
219
219
  prompt_key = key.removeprefix(f"{mounted.prefix}_")
@@ -11,12 +11,15 @@ Tools:
11
11
  * [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)
12
12
  * [annotations](https://gofastmcp.com/servers/tools#annotations-2)
13
13
  * [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)
14
+ * [meta](https://gofastmcp.com/servers/tools#param-meta)
14
15
 
15
16
  Prompts:
16
17
  * [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)
18
+ * [meta](https://gofastmcp.com/servers/prompts#param-meta)
17
19
 
18
20
  Resources:
19
21
  * [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources)
22
+ * [meta](https://gofastmcp.com/servers/resources#param-meta)
20
23
 
21
24
  ## Usage
22
25
 
@@ -78,7 +81,16 @@ class MyComponent(MCPMixin):
78
81
  if delete_all:
79
82
  return "99 records deleted. I bet you're not a tool :)"
80
83
  return "Tool executed, but you might be a tool!"
81
-
84
+
85
+ # example tool w/ meta
86
+ @mcp_tool(
87
+ name="data_tool",
88
+ description="Fetches user data from database",
89
+ meta={"version": "2.0", "category": "database", "author": "dev-team"}
90
+ )
91
+ def data_tool_method(self, user_id: int):
92
+ return f"Fetching data for user {user_id}"
93
+
82
94
  @mcp_resource(uri="component://data")
83
95
  def resource_method(self):
84
96
  return {"data": "some data"}
@@ -88,6 +100,15 @@ class MyComponent(MCPMixin):
88
100
  def resource_method(self):
89
101
  return {"data": "some data"}
90
102
 
103
+ # example resource w/meta and title
104
+ @mcp_resource(
105
+ uri="component://config",
106
+ title="Data resource Title,
107
+ meta={"internal": True, "cache_ttl": 3600, "priority": "high"}
108
+ )
109
+ def config_resource_method(self):
110
+ return {"config": "data"}
111
+
91
112
  # prompt
92
113
  @mcp_prompt(name="A prompt")
93
114
  def prompt_method(self, name):
@@ -98,6 +119,16 @@ class MyComponent(MCPMixin):
98
119
  def prompt_method(self, name):
99
120
  return f"What's up {name}?"
100
121
 
122
+ # example prompt w/title and meta
123
+ @mcp_prompt(
124
+ name="analysis_prompt",
125
+ title="Data Analysis Prompt",
126
+ description="Analyzes data patterns",
127
+ meta={"complexity": "high", "domain": "analytics", "requires_context": True}
128
+ )
129
+ def analysis_prompt_method(self, dataset: str):
130
+ return f"Analyze the patterns in {dataset}"
131
+
101
132
  mcp_server = FastMCP()
102
133
  component = MyComponent()
103
134
 
@@ -2,7 +2,7 @@ from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt
2
2
 
3
3
  __all__ = [
4
4
  "MCPMixin",
5
- "mcp_tool",
6
- "mcp_resource",
7
5
  "mcp_prompt",
6
+ "mcp_resource",
7
+ "mcp_tool",
8
8
  ]
@@ -3,7 +3,7 @@
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from mcp.types import ToolAnnotations
6
+ from mcp.types import Annotations, ToolAnnotations
7
7
 
8
8
  from fastmcp.prompts.prompt import Prompt
9
9
  from fastmcp.resources.resource import Resource
@@ -29,6 +29,7 @@ def mcp_tool(
29
29
  annotations: ToolAnnotations | dict[str, Any] | None = None,
30
30
  exclude_args: list[str] | None = None,
31
31
  serializer: Callable[[Any], str] | None = None,
32
+ meta: dict[str, Any] | None = None,
32
33
  enabled: bool | None = None,
33
34
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
34
35
  """Decorator to mark a method as an MCP tool for later registration."""
@@ -41,6 +42,7 @@ def mcp_tool(
41
42
  "annotations": annotations,
42
43
  "exclude_args": exclude_args,
43
44
  "serializer": serializer,
45
+ "meta": meta,
44
46
  "enabled": enabled,
45
47
  }
46
48
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -54,9 +56,12 @@ def mcp_resource(
54
56
  uri: str,
55
57
  *,
56
58
  name: str | None = None,
59
+ title: str | None = None,
57
60
  description: str | None = None,
58
61
  mime_type: str | None = None,
59
62
  tags: set[str] | None = None,
63
+ annotations: Annotations | None = None,
64
+ meta: dict[str, Any] | None = None,
60
65
  enabled: bool | None = None,
61
66
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
62
67
  """Decorator to mark a method as an MCP resource for later registration."""
@@ -65,9 +70,12 @@ def mcp_resource(
65
70
  call_args = {
66
71
  "uri": uri,
67
72
  "name": name or get_fn_name(func),
73
+ "title": title,
68
74
  "description": description,
69
75
  "mime_type": mime_type,
70
76
  "tags": tags,
77
+ "annotations": annotations,
78
+ "meta": meta,
71
79
  "enabled": enabled,
72
80
  }
73
81
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -81,8 +89,10 @@ def mcp_resource(
81
89
 
82
90
  def mcp_prompt(
83
91
  name: str | None = None,
92
+ title: str | None = None,
84
93
  description: str | None = None,
85
94
  tags: set[str] | None = None,
95
+ meta: dict[str, Any] | None = None,
86
96
  enabled: bool | None = None,
87
97
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
88
98
  """Decorator to mark a method as an MCP prompt for later registration."""
@@ -90,8 +100,10 @@ def mcp_prompt(
90
100
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
91
101
  call_args = {
92
102
  "name": name or get_fn_name(func),
103
+ "title": title,
93
104
  "description": description,
94
105
  "tags": tags,
106
+ "meta": meta,
95
107
  "enabled": enabled,
96
108
  }
97
109
 
@@ -151,7 +163,6 @@ class MCPMixin:
151
163
  tool = Tool.from_function(
152
164
  fn=method,
153
165
  name=registration_info.get("name"),
154
- title=registration_info.get("title"),
155
166
  description=registration_info.get("description"),
156
167
  tags=registration_info.get("tags"),
157
168
  annotations=registration_info.get("annotations"),
@@ -195,6 +206,7 @@ class MCPMixin:
195
206
  fn=method,
196
207
  uri=registration_info["uri"],
197
208
  name=registration_info.get("name"),
209
+ title=registration_info.get("title"),
198
210
  description=registration_info.get("description"),
199
211
  mime_type=registration_info.get("mime_type"),
200
212
  tags=registration_info.get("tags"),
@@ -21,10 +21,10 @@ try:
21
21
  ChatCompletionUserMessageParam,
22
22
  )
23
23
  from openai.types.shared.chat_model import ChatModel
24
- except ImportError:
24
+ except ImportError as e:
25
25
  raise ImportError(
26
26
  "The `openai` package is not installed. Please install `fastmcp[openai]` or add `openai` to your dependencies manually."
27
- )
27
+ ) from e
28
28
 
29
29
  from typing_extensions import override
30
30
 
@@ -22,17 +22,14 @@ from .components import (
22
22
 
23
23
  # Export public symbols - maintaining backward compatibility
24
24
  __all__ = [
25
- # Server
25
+ "DEFAULT_ROUTE_MAPPINGS",
26
+ "ComponentFn",
26
27
  "FastMCPOpenAPI",
27
- # Routing
28
28
  "MCPType",
29
+ "OpenAPIResource",
30
+ "OpenAPIResourceTemplate",
31
+ "OpenAPITool",
29
32
  "RouteMap",
30
33
  "RouteMapFn",
31
- "ComponentFn",
32
- "DEFAULT_ROUTE_MAPPINGS",
33
34
  "_determine_route_type",
34
- # Components
35
- "OpenAPITool",
36
- "OpenAPIResource",
37
- "OpenAPIResourceTemplate",
38
35
  ]
@@ -146,11 +146,11 @@ class OpenAPITool(Tool):
146
146
  if e.response.text:
147
147
  error_message += f" - {e.response.text}"
148
148
 
149
- raise ValueError(error_message)
149
+ raise ValueError(error_message) from e
150
150
 
151
151
  except httpx.RequestError as e:
152
152
  # Handle request errors (connection, timeout, etc.)
153
- raise ValueError(f"Request error: {str(e)}")
153
+ raise ValueError(f"Request error: {e!s}") from e
154
154
 
155
155
 
156
156
  class OpenAPIResource(Resource):
@@ -165,9 +165,11 @@ class OpenAPIResource(Resource):
165
165
  name: str,
166
166
  description: str,
167
167
  mime_type: str = "application/json",
168
- tags: set[str] = set(),
168
+ tags: set[str] | None = None,
169
169
  timeout: float | None = None,
170
170
  ):
171
+ if tags is None:
172
+ tags = set()
171
173
  super().__init__(
172
174
  uri=AnyUrl(uri), # Convert string to AnyUrl
173
175
  name=name,
@@ -276,11 +278,11 @@ class OpenAPIResource(Resource):
276
278
  if e.response.text:
277
279
  error_message += f" - {e.response.text}"
278
280
 
279
- raise ValueError(error_message)
281
+ raise ValueError(error_message) from e
280
282
 
281
283
  except httpx.RequestError as e:
282
284
  # Handle request errors (connection, timeout, etc.)
283
- raise ValueError(f"Request error: {str(e)}")
285
+ raise ValueError(f"Request error: {e!s}") from e
284
286
 
285
287
 
286
288
  class OpenAPIResourceTemplate(ResourceTemplate):
@@ -295,9 +297,11 @@ class OpenAPIResourceTemplate(ResourceTemplate):
295
297
  name: str,
296
298
  description: str,
297
299
  parameters: dict[str, Any],
298
- tags: set[str] = set(),
300
+ tags: set[str] | None = None,
299
301
  timeout: float | None = None,
300
302
  ):
303
+ if tags is None:
304
+ tags = set()
301
305
  super().__init__(
302
306
  uri_template=uri_template,
303
307
  name=name,
@@ -342,7 +346,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
342
346
 
343
347
  # Export public symbols
344
348
  __all__ = [
345
- "OpenAPITool",
346
349
  "OpenAPIResource",
347
350
  "OpenAPIResourceTemplate",
351
+ "OpenAPITool",
348
352
  ]
@@ -121,10 +121,10 @@ def _determine_route_type(
121
121
 
122
122
  # Export public symbols
123
123
  __all__ = [
124
+ "DEFAULT_ROUTE_MAPPINGS",
125
+ "ComponentFn",
124
126
  "MCPType",
125
127
  "RouteMap",
126
128
  "RouteMapFn",
127
- "ComponentFn",
128
- "DEFAULT_ROUTE_MAPPINGS",
129
129
  "_determine_route_type",
130
130
  ]
@@ -40,29 +40,24 @@ from .json_schema_converter import (
40
40
 
41
41
  # Export public symbols - maintaining backward compatibility
42
42
  __all__ = [
43
- # Models
44
43
  "HTTPRoute",
44
+ "HttpMethod",
45
+ "JsonSchema",
45
46
  "ParameterInfo",
47
+ "ParameterLocation",
46
48
  "RequestBodyInfo",
47
49
  "ResponseInfo",
48
- "HttpMethod",
49
- "ParameterLocation",
50
- "JsonSchema",
51
- # Parser
52
- "parse_openapi_to_http_routes",
53
- # Formatters
50
+ "_combine_schemas",
51
+ "_make_optional_parameter_nullable",
52
+ "clean_schema_for_display",
53
+ "convert_openapi_schema_to_json_schema",
54
+ "convert_schema_definitions",
55
+ "extract_output_schema_from_responses",
54
56
  "format_array_parameter",
55
57
  "format_deep_object_parameter",
56
58
  "format_description_with_responses",
57
59
  "format_json_for_description",
58
60
  "format_simple_description",
59
61
  "generate_example_from_schema",
60
- # Schemas
61
- "_combine_schemas",
62
- "extract_output_schema_from_responses",
63
- "clean_schema_for_display",
64
- "_make_optional_parameter_nullable",
65
- # JSON Schema Converter
66
- "convert_openapi_schema_to_json_schema",
67
- "convert_schema_definitions",
62
+ "parse_openapi_to_http_routes",
68
63
  ]
@@ -54,28 +54,27 @@ class RequestDirector:
54
54
  url = self._build_url(route.path, path_params, base_url)
55
55
 
56
56
  # Step 3: Prepare request data
57
- request_data = {
58
- "method": route.method.upper(),
59
- "url": url,
60
- "params": query_params if query_params else None,
61
- "headers": header_params if header_params else None,
62
- }
57
+ method: str = route.method.upper()
58
+ params = query_params if query_params else None
59
+ headers = header_params if header_params else None
60
+ json_body: dict[str, Any] | list[Any] | None = None
61
+ content: str | bytes | None = None
63
62
 
64
63
  # Step 4: Handle request body
65
64
  if body is not None:
66
- if isinstance(body, dict) or isinstance(body, list):
67
- request_data["json"] = body
65
+ if isinstance(body, dict | list):
66
+ json_body = body
68
67
  else:
69
- request_data["content"] = body
68
+ content = body
70
69
 
71
70
  # Step 5: Create httpx.Request
72
71
  return httpx.Request(
73
- method=request_data["method"],
74
- url=request_data["url"],
75
- params=request_data.get("params"),
76
- headers=request_data.get("headers"),
77
- json=request_data.get("json"),
78
- content=request_data.get("content"),
72
+ method=method,
73
+ url=url,
74
+ params=params,
75
+ headers=headers,
76
+ json=json_body,
77
+ content=content,
79
78
  )
80
79
 
81
80
  def _unflatten_arguments(
@@ -164,10 +164,10 @@ def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
164
164
  if isinstance(current_type, str):
165
165
  result["type"] = [current_type, "null"]
166
166
  elif isinstance(current_type, list) and "null" not in current_type:
167
- result["type"] = current_type + ["null"]
167
+ result["type"] = [*current_type, "null"]
168
168
  elif "oneOf" in result:
169
169
  # Convert oneOf to anyOf with null
170
- result["anyOf"] = result.pop("oneOf") + [{"type": "null"}]
170
+ result["anyOf"] = [*result.pop("oneOf"), {"type": "null"}]
171
171
  elif "anyOf" in result:
172
172
  # Add null to anyOf if not present
173
173
  if not any(item.get("type") == "null" for item in result["anyOf"]):
@@ -176,6 +176,10 @@ def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
176
176
  # Wrap allOf in anyOf with null option
177
177
  result["anyOf"] = [{"allOf": result.pop("allOf")}, {"type": "null"}]
178
178
 
179
+ # Handle enum fields - add null to enum values if present
180
+ if "enum" in result and None not in result["enum"]:
181
+ result["enum"] = result["enum"] + [None]
182
+
179
183
  return result
180
184
 
181
185
 
@@ -79,10 +79,10 @@ class HTTPRoute(FastMCPBaseModel):
79
79
  # Export public symbols
80
80
  __all__ = [
81
81
  "HTTPRoute",
82
+ "HttpMethod",
83
+ "JsonSchema",
82
84
  "ParameterInfo",
85
+ "ParameterLocation",
83
86
  "RequestBodyInfo",
84
87
  "ResponseInfo",
85
- "HttpMethod",
86
- "ParameterLocation",
87
- "JsonSchema",
88
88
  ]
@@ -178,7 +178,7 @@ class OpenAPIParser(
178
178
  else:
179
179
  # Special handling for components
180
180
  if part == "components" and hasattr(target, "components"):
181
- target = getattr(target, "components")
181
+ target = target.components
182
182
  elif hasattr(target, part): # Fallback check
183
183
  target = getattr(target, part, None)
184
184
  else:
@@ -474,9 +474,22 @@ class OpenAPIParser(
474
474
  and media_type_obj.media_type_schema
475
475
  ):
476
476
  try:
477
- schema_dict = self._extract_schema_as_dict(
478
- media_type_obj.media_type_schema
479
- )
477
+ # Track if this is a top-level $ref before resolution
478
+ top_level_schema_name = None
479
+ media_schema = media_type_obj.media_type_schema
480
+ if isinstance(media_schema, self.reference_cls):
481
+ ref_str = media_schema.ref
482
+ if isinstance(ref_str, str) and ref_str.startswith(
483
+ "#/components/schemas/"
484
+ ):
485
+ top_level_schema_name = ref_str.split("/")[-1]
486
+
487
+ schema_dict = self._extract_schema_as_dict(media_schema)
488
+ # Add marker for top-level schema if it was a ref
489
+ if top_level_schema_name:
490
+ schema_dict["x-fastmcp-top-level-schema"] = (
491
+ top_level_schema_name
492
+ )
480
493
  resp_info.content_schema[media_type_str] = schema_dict
481
494
  except ValueError as e:
482
495
  # Re-raise ValueError for external reference errors
@@ -541,9 +554,7 @@ class OpenAPIParser(
541
554
  if "$ref" in obj and isinstance(obj["$ref"], str):
542
555
  ref = obj["$ref"]
543
556
  # Handle both converted and unconverted refs
544
- if ref.startswith("#/$defs/"):
545
- schema_name = ref.split("/")[-1]
546
- elif ref.startswith("#/components/schemas/"):
557
+ if ref.startswith(("#/$defs/", "#/components/schemas/")):
547
558
  schema_name = ref.split("/")[-1]
548
559
  else:
549
560
  return
@@ -619,18 +630,28 @@ class OpenAPIParser(
619
630
  Returns:
620
631
  Dictionary containing only the schemas needed for outputs
621
632
  """
622
- needed_schemas = set()
633
+ if not responses or not all_schemas:
634
+ return {}
635
+
636
+ needed_schemas: set[str] = set()
623
637
 
624
- # Check responses for schema references
625
638
  for response in responses.values():
626
- if response.content_schema:
627
- for content_schema in response.content_schema.values():
628
- deps = self._extract_schema_dependencies(
629
- content_schema, all_schemas
639
+ if not response.content_schema:
640
+ continue
641
+
642
+ for content_schema in response.content_schema.values():
643
+ deps = self._extract_schema_dependencies(content_schema, all_schemas)
644
+ needed_schemas.update(deps)
645
+
646
+ schema_name = content_schema.get("x-fastmcp-top-level-schema")
647
+ if isinstance(schema_name, str) and schema_name in all_schemas:
648
+ needed_schemas.add(schema_name)
649
+ self._extract_schema_dependencies(
650
+ all_schemas[schema_name],
651
+ all_schemas,
652
+ collected=needed_schemas,
630
653
  )
631
- needed_schemas.update(deps)
632
654
 
633
- # Return only the needed output schemas
634
655
  return {
635
656
  name: all_schemas[name] for name in needed_schemas if name in all_schemas
636
657
  }
@@ -795,6 +816,6 @@ class OpenAPIParser(
795
816
 
796
817
  # Export public symbols
797
818
  __all__ = [
798
- "parse_openapi_to_http_routes",
799
819
  "OpenAPIParser",
820
+ "parse_openapi_to_http_routes",
800
821
  ]
@@ -585,9 +585,9 @@ def extract_output_schema_from_responses(
585
585
 
586
586
  # Export public symbols
587
587
  __all__ = [
588
- "clean_schema_for_display",
589
588
  "_combine_schemas",
590
589
  "_combine_schemas_and_map_params",
591
- "extract_output_schema_from_responses",
592
590
  "_make_optional_parameter_nullable",
591
+ "clean_schema_for_display",
592
+ "extract_output_schema_from_responses",
593
593
  ]
fastmcp/mcp_config.py CHANGED
@@ -101,7 +101,7 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
101
101
  ClientTransport, # pyright: ignore[reportUnusedImport]
102
102
  )
103
103
 
104
- transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
104
+ transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] # ty: ignore[unresolved-attribute]
105
105
  transport = cast(ClientTransport, transport)
106
106
 
107
107
  client: Client[ClientTransport] = Client(transport=transport, name=client_name)
@@ -288,9 +288,8 @@ class MCPConfig(BaseModel):
288
288
  @classmethod
289
289
  def from_file(cls, file_path: Path) -> Self:
290
290
  """Load configuration from JSON file."""
291
- if file_path.exists():
292
- if content := file_path.read_text().strip():
293
- return cls.model_validate_json(content)
291
+ if file_path.exists() and (content := file_path.read_text().strip()):
292
+ return cls.model_validate_json(content)
294
293
 
295
294
  raise ValueError(f"No MCP servers defined in the config: {file_path}")
296
295
 
@@ -2,8 +2,8 @@ from .prompt import Prompt, PromptMessage, Message
2
2
  from .prompt_manager import PromptManager
3
3
 
4
4
  __all__ = [
5
+ "Message",
5
6
  "Prompt",
6
7
  "PromptManager",
7
8
  "PromptMessage",
8
- "Message",
9
9
  ]