fastmcp 2.13.0rc3__py3-none-any.whl → 2.13.0.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 +2 -2
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +7 -6
- fastmcp/client/client.py +10 -10
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +34 -34
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/mcp_mixin/__init__.py +2 -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 +1 -1
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +3 -5
- fastmcp/experimental/utilities/openapi/schemas.py +2 -2
- fastmcp/mcp_config.py +2 -3
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +9 -13
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +1 -3
- fastmcp/resources/resource_manager.py +1 -1
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +5 -5
- fastmcp/server/auth/auth.py +2 -2
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/azure.py +48 -25
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/in_memory.py +2 -2
- fastmcp/server/auth/providers/introspection.py +2 -2
- fastmcp/server/auth/providers/jwt.py +17 -18
- fastmcp/server/auth/providers/supabase.py +1 -1
- fastmcp/server/auth/providers/workos.py +2 -2
- fastmcp/server/context.py +8 -10
- fastmcp/server/dependencies.py +5 -6
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +2 -3
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +1 -1
- fastmcp/server/middleware/error_handling.py +8 -8
- fastmcp/server/middleware/middleware.py +1 -1
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +5 -4
- fastmcp/server/server.py +27 -29
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +12 -12
- fastmcp/tools/tool_transform.py +6 -6
- fastmcp/utilities/cli.py +5 -6
- fastmcp/utilities/inspect.py +2 -2
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +14 -18
- 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/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +9 -9
- fastmcp/utilities/tests.py +2 -4
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/METADATA +3 -3
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/RECORD +65 -65
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.0rc3.dist-info → fastmcp-2.13.0.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/__init__.py
CHANGED
fastmcp/cli/cli.py
CHANGED
|
@@ -78,7 +78,7 @@ def with_argv(args: list[str] | None):
|
|
|
78
78
|
original = sys.argv[:]
|
|
79
79
|
try:
|
|
80
80
|
# Preserve the script name (sys.argv[0]) and replace the rest
|
|
81
|
-
sys.argv = [sys.argv[0]
|
|
81
|
+
sys.argv = [sys.argv[0], *args]
|
|
82
82
|
yield
|
|
83
83
|
finally:
|
|
84
84
|
sys.argv = original
|
|
@@ -277,7 +277,7 @@ async def dev(
|
|
|
277
277
|
|
|
278
278
|
# Run the MCP Inspector command
|
|
279
279
|
process = subprocess.run(
|
|
280
|
-
[npx_cmd, inspector_cmd
|
|
280
|
+
[npx_cmd, inspector_cmd, *uv_cmd],
|
|
281
281
|
check=True,
|
|
282
282
|
env=env,
|
|
283
283
|
)
|
fastmcp/client/__init__.py
CHANGED
|
@@ -15,18 +15,18 @@ from .transports import (
|
|
|
15
15
|
from .auth import OAuth, BearerAuth
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
|
+
"BearerAuth",
|
|
18
19
|
"Client",
|
|
19
20
|
"ClientTransport",
|
|
20
|
-
"
|
|
21
|
-
"SSETransport",
|
|
22
|
-
"StdioTransport",
|
|
23
|
-
"PythonStdioTransport",
|
|
21
|
+
"FastMCPTransport",
|
|
24
22
|
"NodeStdioTransport",
|
|
25
|
-
"UvxStdioTransport",
|
|
26
|
-
"UvStdioTransport",
|
|
27
23
|
"NpxStdioTransport",
|
|
28
|
-
"FastMCPTransport",
|
|
29
|
-
"StreamableHttpTransport",
|
|
30
24
|
"OAuth",
|
|
31
|
-
"
|
|
25
|
+
"PythonStdioTransport",
|
|
26
|
+
"SSETransport",
|
|
27
|
+
"StdioTransport",
|
|
28
|
+
"StreamableHttpTransport",
|
|
29
|
+
"UvStdioTransport",
|
|
30
|
+
"UvxStdioTransport",
|
|
31
|
+
"WSTransport",
|
|
32
32
|
]
|
fastmcp/client/auth/oauth.py
CHANGED
|
@@ -36,8 +36,6 @@ logger = get_logger(__name__)
|
|
|
36
36
|
class ClientNotFoundError(Exception):
|
|
37
37
|
"""Raised when OAuth client credentials are not found on the server."""
|
|
38
38
|
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
39
|
|
|
42
40
|
async def check_if_auth_required(
|
|
43
41
|
mcp_url: str, httpx_kwargs: dict[str, Any] | None = None
|
|
@@ -58,7 +56,7 @@ async def check_if_auth_required(
|
|
|
58
56
|
return True
|
|
59
57
|
|
|
60
58
|
# Check for WWW-Authenticate header
|
|
61
|
-
if "WWW-Authenticate" in response.headers:
|
|
59
|
+
if "WWW-Authenticate" in response.headers: # noqa: SIM103
|
|
62
60
|
return True
|
|
63
61
|
|
|
64
62
|
# If we get a successful response, auth may not be required
|
|
@@ -195,7 +193,8 @@ class OAuth(OAuthClientProvider):
|
|
|
195
193
|
|
|
196
194
|
warn(
|
|
197
195
|
message="Using in-memory token storage is not recommended for production use -- "
|
|
198
|
-
+ "tokens will be lost on server restart."
|
|
196
|
+
+ "tokens will be lost on server restart.",
|
|
197
|
+
stacklevel=2,
|
|
199
198
|
)
|
|
200
199
|
|
|
201
200
|
self.token_storage_adapter: TokenStorageAdapter = TokenStorageAdapter(
|
|
@@ -272,8 +271,10 @@ class OAuth(OAuthClientProvider):
|
|
|
272
271
|
if result.error:
|
|
273
272
|
raise result.error
|
|
274
273
|
return result.code, result.state # type: ignore
|
|
275
|
-
except TimeoutError:
|
|
276
|
-
raise TimeoutError(
|
|
274
|
+
except TimeoutError as e:
|
|
275
|
+
raise TimeoutError(
|
|
276
|
+
f"OAuth callback timed out after {TIMEOUT} seconds"
|
|
277
|
+
) from e
|
|
277
278
|
finally:
|
|
278
279
|
server.should_exit = True
|
|
279
280
|
await anyio.sleep(0.1) # Allow server to shut down gracefully
|
fastmcp/client/client.py
CHANGED
|
@@ -61,15 +61,15 @@ from .transports import (
|
|
|
61
61
|
|
|
62
62
|
__all__ = [
|
|
63
63
|
"Client",
|
|
64
|
-
"SessionKwargs",
|
|
65
|
-
"RootsHandler",
|
|
66
|
-
"RootsList",
|
|
67
|
-
"LogHandler",
|
|
68
|
-
"MessageHandler",
|
|
69
64
|
"ClientSamplingHandler",
|
|
70
|
-
"SamplingHandler",
|
|
71
65
|
"ElicitationHandler",
|
|
66
|
+
"LogHandler",
|
|
67
|
+
"MessageHandler",
|
|
72
68
|
"ProgressHandler",
|
|
69
|
+
"RootsHandler",
|
|
70
|
+
"RootsList",
|
|
71
|
+
"SamplingHandler",
|
|
72
|
+
"SessionKwargs",
|
|
73
73
|
]
|
|
74
74
|
|
|
75
75
|
logger = get_logger(__name__)
|
|
@@ -362,10 +362,10 @@ class Client(Generic[ClientTransportT]):
|
|
|
362
362
|
await self._session_state.session.initialize()
|
|
363
363
|
)
|
|
364
364
|
yield
|
|
365
|
-
except anyio.ClosedResourceError:
|
|
366
|
-
raise RuntimeError("Server session was closed unexpectedly")
|
|
367
|
-
except TimeoutError:
|
|
368
|
-
raise RuntimeError("Failed to initialize server session")
|
|
365
|
+
except anyio.ClosedResourceError as e:
|
|
366
|
+
raise RuntimeError("Server session was closed unexpectedly") from e
|
|
367
|
+
except TimeoutError as e:
|
|
368
|
+
raise RuntimeError("Failed to initialize server session") from e
|
|
369
369
|
finally:
|
|
370
370
|
self._session_state.session = None
|
|
371
371
|
self._session_state.initialize_result = None
|
fastmcp/client/sampling.py
CHANGED
|
@@ -11,7 +11,7 @@ from mcp.types import SamplingMessage
|
|
|
11
11
|
|
|
12
12
|
from fastmcp.server.sampling.handler import ServerSamplingHandler
|
|
13
13
|
|
|
14
|
-
__all__ = ["
|
|
14
|
+
__all__ = ["SamplingHandler", "SamplingMessage", "SamplingParams"]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
ClientSamplingHandler: TypeAlias = Callable[
|
fastmcp/client/transports.py
CHANGED
|
@@ -46,16 +46,16 @@ ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
|
|
|
46
46
|
|
|
47
47
|
__all__ = [
|
|
48
48
|
"ClientTransport",
|
|
49
|
-
"SSETransport",
|
|
50
|
-
"StreamableHttpTransport",
|
|
51
|
-
"StdioTransport",
|
|
52
|
-
"PythonStdioTransport",
|
|
53
49
|
"FastMCPStdioTransport",
|
|
50
|
+
"FastMCPTransport",
|
|
54
51
|
"NodeStdioTransport",
|
|
55
|
-
"UvxStdioTransport",
|
|
56
|
-
"UvStdioTransport",
|
|
57
52
|
"NpxStdioTransport",
|
|
58
|
-
"
|
|
53
|
+
"PythonStdioTransport",
|
|
54
|
+
"SSETransport",
|
|
55
|
+
"StdioTransport",
|
|
56
|
+
"StreamableHttpTransport",
|
|
57
|
+
"UvStdioTransport",
|
|
58
|
+
"UvxStdioTransport",
|
|
59
59
|
"infer_transport",
|
|
60
60
|
]
|
|
61
61
|
|
|
@@ -109,9 +109,8 @@ class ClientTransport(abc.ABC):
|
|
|
109
109
|
# Basic representation for subclasses
|
|
110
110
|
return f"<{self.__class__.__name__}>"
|
|
111
111
|
|
|
112
|
-
async def close(self):
|
|
112
|
+
async def close(self): # noqa: B027
|
|
113
113
|
"""Close the transport."""
|
|
114
|
-
pass
|
|
115
114
|
|
|
116
115
|
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
117
116
|
if auth is not None:
|
|
@@ -141,10 +140,10 @@ class WSTransport(ClientTransport):
|
|
|
141
140
|
) -> AsyncIterator[ClientSession]:
|
|
142
141
|
try:
|
|
143
142
|
from mcp.client.websocket import websocket_client
|
|
144
|
-
except ImportError:
|
|
143
|
+
except ImportError as e:
|
|
145
144
|
raise ImportError(
|
|
146
145
|
"The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
|
|
147
|
-
)
|
|
146
|
+
) from e
|
|
148
147
|
|
|
149
148
|
async with websocket_client(self.url) as transport:
|
|
150
149
|
read_stream, write_stream = transport
|
|
@@ -207,7 +206,7 @@ class SSETransport(ClientTransport):
|
|
|
207
206
|
# instead we simply leave the kwarg out if it's not provided
|
|
208
207
|
if self.sse_read_timeout is not None:
|
|
209
208
|
client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
|
|
210
|
-
if session_kwargs.get("read_timeout_seconds"
|
|
209
|
+
if session_kwargs.get("read_timeout_seconds") is not None:
|
|
211
210
|
read_timeout_seconds = cast(
|
|
212
211
|
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
|
|
213
212
|
)
|
|
@@ -277,7 +276,7 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
277
276
|
# instead we simply leave the kwarg out if it's not provided
|
|
278
277
|
if self.sse_read_timeout is not None:
|
|
279
278
|
client_kwargs["sse_read_timeout"] = self.sse_read_timeout
|
|
280
|
-
if session_kwargs.get("read_timeout_seconds"
|
|
279
|
+
if session_kwargs.get("read_timeout_seconds") is not None:
|
|
281
280
|
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
|
282
281
|
|
|
283
282
|
if self.httpx_client_factory is not None:
|
|
@@ -451,8 +450,7 @@ async def _stdio_transport_connect_task(
|
|
|
451
450
|
if log_file is None:
|
|
452
451
|
log_file_handle = sys.stderr
|
|
453
452
|
elif isinstance(log_file, Path):
|
|
454
|
-
log_file_handle = open(
|
|
455
|
-
stack.callback(log_file_handle.close)
|
|
453
|
+
log_file_handle = stack.enter_context(log_file.open("a"))
|
|
456
454
|
else:
|
|
457
455
|
# Must be TextIO - use it directly
|
|
458
456
|
log_file_handle = log_file
|
|
@@ -852,26 +850,28 @@ class FastMCPTransport(ClientTransport):
|
|
|
852
850
|
server_read, server_write = server_streams
|
|
853
851
|
|
|
854
852
|
# Create a cancel scope for the server task
|
|
855
|
-
async with
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
)
|
|
853
|
+
async with (
|
|
854
|
+
anyio.create_task_group() as tg,
|
|
855
|
+
_enter_server_lifespan(server=self.server),
|
|
856
|
+
):
|
|
857
|
+
tg.start_soon(
|
|
858
|
+
lambda: self.server._mcp_server.run(
|
|
859
|
+
server_read,
|
|
860
|
+
server_write,
|
|
861
|
+
self.server._mcp_server.create_initialization_options(),
|
|
862
|
+
raise_exceptions=self.raise_exceptions,
|
|
864
863
|
)
|
|
864
|
+
)
|
|
865
865
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
866
|
+
try:
|
|
867
|
+
async with ClientSession(
|
|
868
|
+
read_stream=client_read,
|
|
869
|
+
write_stream=client_write,
|
|
870
|
+
**session_kwargs,
|
|
871
|
+
) as client_session:
|
|
872
|
+
yield client_session
|
|
873
|
+
finally:
|
|
874
|
+
tg.cancel_scope.cancel()
|
|
875
875
|
|
|
876
876
|
def __repr__(self) -> str:
|
|
877
877
|
return f"<FastMCPTransport(server='{self.server.name}')>"
|
|
@@ -952,7 +952,7 @@ class MCPConfigTransport(ClientTransport):
|
|
|
952
952
|
|
|
953
953
|
# if there's exactly one server, create a client for that server
|
|
954
954
|
elif len(self.config.mcpServers) == 1:
|
|
955
|
-
self.transport =
|
|
955
|
+
self.transport = next(iter(self.config.mcpServers.values())).to_transport()
|
|
956
956
|
self._underlying_transports.append(self.transport)
|
|
957
957
|
|
|
958
958
|
# otherwise create a composite client
|
|
@@ -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
|
|
|
@@ -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
|
]
|
|
@@ -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"]):
|
|
@@ -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:
|
|
@@ -554,9 +554,7 @@ class OpenAPIParser(
|
|
|
554
554
|
if "$ref" in obj and isinstance(obj["$ref"], str):
|
|
555
555
|
ref = obj["$ref"]
|
|
556
556
|
# Handle both converted and unconverted refs
|
|
557
|
-
if ref.startswith("#/$defs/"):
|
|
558
|
-
schema_name = ref.split("/")[-1]
|
|
559
|
-
elif ref.startswith("#/components/schemas/"):
|
|
557
|
+
if ref.startswith(("#/$defs/", "#/components/schemas/")):
|
|
560
558
|
schema_name = ref.split("/")[-1]
|
|
561
559
|
else:
|
|
562
560
|
return
|
|
@@ -815,6 +813,6 @@ class OpenAPIParser(
|
|
|
815
813
|
|
|
816
814
|
# Export public symbols
|
|
817
815
|
__all__ = [
|
|
818
|
-
"parse_openapi_to_http_routes",
|
|
819
816
|
"OpenAPIParser",
|
|
817
|
+
"parse_openapi_to_http_routes",
|
|
820
818
|
]
|
|
@@ -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
|
@@ -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
|
|
fastmcp/prompts/__init__.py
CHANGED
fastmcp/prompts/prompt.py
CHANGED
|
@@ -207,10 +207,7 @@ class FunctionPrompt(Prompt):
|
|
|
207
207
|
# Auto-detect context parameter if not provided
|
|
208
208
|
|
|
209
209
|
context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
|
|
210
|
-
if context_kwarg
|
|
211
|
-
prune_params = [context_kwarg]
|
|
212
|
-
else:
|
|
213
|
-
prune_params = None
|
|
210
|
+
prune_params = [context_kwarg] if context_kwarg else None
|
|
214
211
|
|
|
215
212
|
parameters = compress_schema(parameters, prune_params=prune_params)
|
|
216
213
|
|
|
@@ -290,10 +287,7 @@ class FunctionPrompt(Prompt):
|
|
|
290
287
|
if (
|
|
291
288
|
param.annotation == inspect.Parameter.empty
|
|
292
289
|
or param.annotation is str
|
|
293
|
-
):
|
|
294
|
-
converted_kwargs[param_name] = param_value
|
|
295
|
-
# If argument is not a string, pass as-is (already properly typed)
|
|
296
|
-
elif not isinstance(param_value, str):
|
|
290
|
+
) or not isinstance(param_value, str):
|
|
297
291
|
converted_kwargs[param_name] = param_value
|
|
298
292
|
else:
|
|
299
293
|
# Try to convert string argument using type adapter
|
|
@@ -314,7 +308,7 @@ class FunctionPrompt(Prompt):
|
|
|
314
308
|
raise PromptError(
|
|
315
309
|
f"Could not convert argument '{param_name}' with value '{param_value}' "
|
|
316
310
|
f"to expected type {param.annotation}. Error: {e}"
|
|
317
|
-
)
|
|
311
|
+
) from e
|
|
318
312
|
else:
|
|
319
313
|
# Parameter not in function signature, pass as-is
|
|
320
314
|
converted_kwargs[param_name] = param_value
|
|
@@ -376,10 +370,12 @@ class FunctionPrompt(Prompt):
|
|
|
376
370
|
content=TextContent(type="text", text=content),
|
|
377
371
|
)
|
|
378
372
|
)
|
|
379
|
-
except Exception:
|
|
380
|
-
raise PromptError(
|
|
373
|
+
except Exception as e:
|
|
374
|
+
raise PromptError(
|
|
375
|
+
"Could not convert prompt result to message."
|
|
376
|
+
) from e
|
|
381
377
|
|
|
382
378
|
return messages
|
|
383
|
-
except Exception:
|
|
379
|
+
except Exception as e:
|
|
384
380
|
logger.exception(f"Error rendering prompt {self.name}")
|
|
385
|
-
raise PromptError(f"Error rendering prompt {self.name}.")
|
|
381
|
+
raise PromptError(f"Error rendering prompt {self.name}.") from e
|
fastmcp/resources/__init__.py
CHANGED
|
@@ -10,13 +10,13 @@ from .types import (
|
|
|
10
10
|
from .resource_manager import ResourceManager
|
|
11
11
|
|
|
12
12
|
__all__ = [
|
|
13
|
-
"Resource",
|
|
14
|
-
"TextResource",
|
|
15
13
|
"BinaryResource",
|
|
16
|
-
"
|
|
14
|
+
"DirectoryResource",
|
|
17
15
|
"FileResource",
|
|
16
|
+
"FunctionResource",
|
|
18
17
|
"HttpResource",
|
|
19
|
-
"
|
|
20
|
-
"ResourceTemplate",
|
|
18
|
+
"Resource",
|
|
21
19
|
"ResourceManager",
|
|
20
|
+
"ResourceTemplate",
|
|
21
|
+
"TextResource",
|
|
22
22
|
]
|
fastmcp/resources/resource.py
CHANGED
|
@@ -217,9 +217,7 @@ class FunctionResource(Resource):
|
|
|
217
217
|
|
|
218
218
|
if isinstance(result, Resource):
|
|
219
219
|
return await result.read()
|
|
220
|
-
elif isinstance(result, bytes):
|
|
221
|
-
return result
|
|
222
|
-
elif isinstance(result, str):
|
|
220
|
+
elif isinstance(result, bytes | str):
|
|
223
221
|
return result
|
|
224
222
|
else:
|
|
225
223
|
return pydantic_core.to_json(result, fallback=str).decode()
|