fastmcp 2.12.1__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 (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.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,
@@ -174,7 +174,7 @@ class ComponentService:
174
174
  key: The key of the prompt to enable
175
175
 
176
176
  Returns:
177
- The prompt that was enable
177
+ The prompt that was enabled
178
178
  """
179
179
  logger.debug("Enabling prompt: %s", key)
180
180
 
@@ -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
- * [enable/disabe](https://gofastmcp.com/servers/resources#disabling-resources)
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,15 +100,34 @@ 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):
94
- return f"Whats up {name}?"
115
+ return f"What's up {name}?"
95
116
 
96
117
  # disabled prompt
97
118
  @mcp_prompt(name="A prompt", enabled=False)
98
119
  def prompt_method(self, name):
99
- return f"Whats up {name}?"
120
+ return f"What's up {name}?"
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}"
100
131
 
101
132
  mcp_server = FastMCP()
102
133
  component = MyComponent()
@@ -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,11 +3,12 @@
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
10
10
  from fastmcp.tools.tool import Tool
11
+ from fastmcp.utilities.types import get_fn_name
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from fastmcp.server import FastMCP
@@ -28,18 +29,20 @@ def mcp_tool(
28
29
  annotations: ToolAnnotations | dict[str, Any] | None = None,
29
30
  exclude_args: list[str] | None = None,
30
31
  serializer: Callable[[Any], str] | None = None,
32
+ meta: dict[str, Any] | None = None,
31
33
  enabled: bool | None = None,
32
34
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
33
35
  """Decorator to mark a method as an MCP tool for later registration."""
34
36
 
35
37
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
36
38
  call_args = {
37
- "name": name or func.__name__,
39
+ "name": name or get_fn_name(func),
38
40
  "description": description,
39
41
  "tags": tags,
40
42
  "annotations": annotations,
41
43
  "exclude_args": exclude_args,
42
44
  "serializer": serializer,
45
+ "meta": meta,
43
46
  "enabled": enabled,
44
47
  }
45
48
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -53,9 +56,12 @@ def mcp_resource(
53
56
  uri: str,
54
57
  *,
55
58
  name: str | None = None,
59
+ title: str | None = None,
56
60
  description: str | None = None,
57
61
  mime_type: str | None = None,
58
62
  tags: set[str] | None = None,
63
+ annotations: Annotations | None = None,
64
+ meta: dict[str, Any] | None = None,
59
65
  enabled: bool | None = None,
60
66
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
61
67
  """Decorator to mark a method as an MCP resource for later registration."""
@@ -63,10 +69,13 @@ def mcp_resource(
63
69
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
64
70
  call_args = {
65
71
  "uri": uri,
66
- "name": name or func.__name__,
72
+ "name": name or get_fn_name(func),
73
+ "title": title,
67
74
  "description": description,
68
75
  "mime_type": mime_type,
69
76
  "tags": tags,
77
+ "annotations": annotations,
78
+ "meta": meta,
70
79
  "enabled": enabled,
71
80
  }
72
81
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -80,17 +89,21 @@ def mcp_resource(
80
89
 
81
90
  def mcp_prompt(
82
91
  name: str | None = None,
92
+ title: str | None = None,
83
93
  description: str | None = None,
84
94
  tags: set[str] | None = None,
95
+ meta: dict[str, Any] | None = None,
85
96
  enabled: bool | None = None,
86
97
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
87
98
  """Decorator to mark a method as an MCP prompt for later registration."""
88
99
 
89
100
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
90
101
  call_args = {
91
- "name": name or func.__name__,
102
+ "name": name or get_fn_name(func),
103
+ "title": title,
92
104
  "description": description,
93
105
  "tags": tags,
106
+ "meta": meta,
94
107
  "enabled": enabled,
95
108
  }
96
109
 
@@ -146,7 +159,20 @@ class MCPMixin:
146
159
  registration_info["name"] = (
147
160
  f"{prefix}{separator}{registration_info['name']}"
148
161
  )
149
- tool = Tool.from_function(fn=method, **registration_info)
162
+
163
+ tool = Tool.from_function(
164
+ fn=method,
165
+ name=registration_info.get("name"),
166
+ description=registration_info.get("description"),
167
+ tags=registration_info.get("tags"),
168
+ annotations=registration_info.get("annotations"),
169
+ exclude_args=registration_info.get("exclude_args"),
170
+ serializer=registration_info.get("serializer"),
171
+ output_schema=registration_info.get("output_schema"),
172
+ meta=registration_info.get("meta"),
173
+ enabled=registration_info.get("enabled"),
174
+ )
175
+
150
176
  mcp_server.add_tool(tool)
151
177
 
152
178
  def register_resources(
@@ -175,7 +201,20 @@ class MCPMixin:
175
201
  registration_info["uri"] = (
176
202
  f"{prefix}{separator}{registration_info['uri']}"
177
203
  )
178
- resource = Resource.from_function(fn=method, **registration_info)
204
+
205
+ resource = Resource.from_function(
206
+ fn=method,
207
+ uri=registration_info["uri"],
208
+ name=registration_info.get("name"),
209
+ title=registration_info.get("title"),
210
+ description=registration_info.get("description"),
211
+ mime_type=registration_info.get("mime_type"),
212
+ tags=registration_info.get("tags"),
213
+ enabled=registration_info.get("enabled"),
214
+ annotations=registration_info.get("annotations"),
215
+ meta=registration_info.get("meta"),
216
+ )
217
+
179
218
  mcp_server.add_resource(resource)
180
219
 
181
220
  def register_prompts(
@@ -200,7 +239,15 @@ class MCPMixin:
200
239
  registration_info["name"] = (
201
240
  f"{prefix}{separator}{registration_info['name']}"
202
241
  )
203
- prompt = Prompt.from_function(fn=method, **registration_info)
242
+ prompt = Prompt.from_function(
243
+ fn=method,
244
+ name=registration_info.get("name"),
245
+ title=registration_info.get("title"),
246
+ description=registration_info.get("description"),
247
+ tags=registration_info.get("tags"),
248
+ enabled=registration_info.get("enabled"),
249
+ meta=registration_info.get("meta"),
250
+ )
204
251
  mcp_server.add_prompt(prompt)
205
252
 
206
253
  def register_all(
@@ -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,22 +54,28 @@ 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
- return httpx.Request(**{k: v for k, v in request_data.items() if v is not None})
71
+ return httpx.Request(
72
+ method=method,
73
+ url=url,
74
+ params=params,
75
+ headers=headers,
76
+ json=json_body,
77
+ content=content,
78
+ )
73
79
 
74
80
  def _unflatten_arguments(
75
81
  self, route: HTTPRoute, flat_args: dict[str, Any]
@@ -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
  ]