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.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- 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 +16 -10
- 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 +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- 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 +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- 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 +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- 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 +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- 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 +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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,
|
|
@@ -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
|
|
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.
|
|
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
|
-
* [enable/
|
|
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"
|
|
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"
|
|
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()
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
-
return httpx.Request(
|
|
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
|
|
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
|
]
|