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.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +11 -11
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +18 -12
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +115 -217
- fastmcp/client/client.py +105 -39
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +80 -25
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +14 -15
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +22 -19
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +14 -9
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +70 -43
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1510 -289
- fastmcp/server/auth/oidc_proxy.py +84 -20
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +312 -131
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +86 -29
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +48 -9
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +35 -17
- fastmcp/server/context.py +177 -51
- fastmcp/server/dependencies.py +39 -12
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +56 -17
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +50 -39
- fastmcp/server/middleware/middleware.py +29 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +725 -242
- fastmcp/settings.py +24 -10
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +70 -23
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +12 -10
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +4 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +85 -4
- fastmcp/utilities/types.py +78 -16
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- fastmcp-2.12.5.dist-info/RECORD +0 -134
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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: {
|
|
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] =
|
|
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: {
|
|
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] =
|
|
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
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
67
|
-
|
|
65
|
+
if isinstance(body, dict | list):
|
|
66
|
+
json_body = body
|
|
68
67
|
else:
|
|
69
|
-
|
|
68
|
+
content = body
|
|
70
69
|
|
|
71
70
|
# Step 5: Create httpx.Request
|
|
72
71
|
return httpx.Request(
|
|
73
|
-
method=
|
|
74
|
-
url=
|
|
75
|
-
params=
|
|
76
|
-
headers=
|
|
77
|
-
json=
|
|
78
|
-
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
|
|
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")
|
|
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 =
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
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
|
|