fastmcp 2.13.0rc2__py3-none-any.whl → 2.13.0.1__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 +3 -2
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +7 -6
- fastmcp/client/client.py +10 -10
- fastmcp/client/oauth_callback.py +6 -2
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +35 -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/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +5 -5
- fastmcp/server/auth/auth.py +2 -2
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +39 -92
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +236 -217
- fastmcp/server/auth/oidc_proxy.py +18 -3
- fastmcp/server/auth/providers/auth0.py +28 -15
- fastmcp/server/auth/providers/aws.py +16 -1
- fastmcp/server/auth/providers/azure.py +101 -40
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/github.py +16 -1
- fastmcp/server/auth/providers/google.py +16 -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 +18 -3
- fastmcp/server/context.py +41 -12
- fastmcp/server/dependencies.py +5 -6
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +3 -4
- 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/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +10 -6
- fastmcp/server/proxy.py +5 -4
- fastmcp/server/server.py +74 -55
- fastmcp/settings.py +2 -1
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +12 -12
- fastmcp/tools/tool_manager.py +8 -4
- fastmcp/tools/tool_transform.py +6 -6
- fastmcp/utilities/cli.py +50 -21
- 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/utilities/ui.py +126 -6
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
- fastmcp-2.13.0.1.dist-info/RECORD +141 -0
- fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.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
|
)
|
|
@@ -772,6 +772,7 @@ async def inspect(
|
|
|
772
772
|
"server_spec": server_spec,
|
|
773
773
|
"error": str(e),
|
|
774
774
|
},
|
|
775
|
+
exc_info=True,
|
|
775
776
|
)
|
|
776
777
|
console.print(f"[bold red]✗[/bold red] Failed to inspect server: {e}")
|
|
777
778
|
sys.exit(1)
|
|
@@ -125,15 +125,15 @@ def install_claude_code(
|
|
|
125
125
|
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
|
126
126
|
|
|
127
127
|
# Build claude mcp add command
|
|
128
|
-
cmd_parts = [claude_cmd, "mcp", "add"]
|
|
128
|
+
cmd_parts = [claude_cmd, "mcp", "add", name]
|
|
129
129
|
|
|
130
|
-
# Add environment variables if specified
|
|
130
|
+
# Add environment variables if specified
|
|
131
131
|
if env_vars:
|
|
132
132
|
for key, value in env_vars.items():
|
|
133
133
|
cmd_parts.extend(["-e", f"{key}={value}"])
|
|
134
134
|
|
|
135
135
|
# Add server name and command
|
|
136
|
-
cmd_parts.
|
|
136
|
+
cmd_parts.append("--")
|
|
137
137
|
cmd_parts.extend(full_command)
|
|
138
138
|
|
|
139
139
|
try:
|
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/oauth_callback.py
CHANGED
|
@@ -46,9 +46,13 @@ def create_callback_html(
|
|
|
46
46
|
# Add detail info box for both success and error cases
|
|
47
47
|
detail_info = ""
|
|
48
48
|
if is_success and server_url:
|
|
49
|
-
detail_info = create_info_box(
|
|
49
|
+
detail_info = create_info_box(
|
|
50
|
+
f"Connected to: {server_url}", centered=True, monospace=True
|
|
51
|
+
)
|
|
50
52
|
elif not is_success:
|
|
51
|
-
detail_info = create_info_box(
|
|
53
|
+
detail_info = create_info_box(
|
|
54
|
+
message, is_error=True, centered=True, monospace=True
|
|
55
|
+
)
|
|
52
56
|
|
|
53
57
|
# Build the page content
|
|
54
58
|
content = f"""
|
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
|
|
@@ -748,6 +746,7 @@ class UvxStdioTransport(StdioTransport):
|
|
|
748
746
|
env: dict[str, str] | None = None
|
|
749
747
|
if env_vars:
|
|
750
748
|
env = os.environ.copy()
|
|
749
|
+
env.update(env_vars)
|
|
751
750
|
|
|
752
751
|
super().__init__(
|
|
753
752
|
command="uvx",
|
|
@@ -851,26 +850,28 @@ class FastMCPTransport(ClientTransport):
|
|
|
851
850
|
server_read, server_write = server_streams
|
|
852
851
|
|
|
853
852
|
# Create a cancel scope for the server task
|
|
854
|
-
async with
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
)
|
|
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,
|
|
863
863
|
)
|
|
864
|
+
)
|
|
864
865
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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()
|
|
874
875
|
|
|
875
876
|
def __repr__(self) -> str:
|
|
876
877
|
return f"<FastMCPTransport(server='{self.server.name}')>"
|
|
@@ -951,7 +952,7 @@ class MCPConfigTransport(ClientTransport):
|
|
|
951
952
|
|
|
952
953
|
# if there's exactly one server, create a client for that server
|
|
953
954
|
elif len(self.config.mcpServers) == 1:
|
|
954
|
-
self.transport =
|
|
955
|
+
self.transport = next(iter(self.config.mcpServers.values())).to_transport()
|
|
955
956
|
self._underlying_transports.append(self.transport)
|
|
956
957
|
|
|
957
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
|
|