fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. fastmcp/_vendor/__init__.py +1 -0
  2. fastmcp/_vendor/docket_di/README.md +7 -0
  3. fastmcp/_vendor/docket_di/__init__.py +163 -0
  4. fastmcp/cli/cli.py +112 -28
  5. fastmcp/cli/install/claude_code.py +1 -5
  6. fastmcp/cli/install/claude_desktop.py +1 -5
  7. fastmcp/cli/install/cursor.py +1 -5
  8. fastmcp/cli/install/gemini_cli.py +1 -5
  9. fastmcp/cli/install/mcp_json.py +1 -6
  10. fastmcp/cli/run.py +146 -5
  11. fastmcp/client/__init__.py +7 -9
  12. fastmcp/client/auth/oauth.py +18 -17
  13. fastmcp/client/client.py +100 -870
  14. fastmcp/client/elicitation.py +1 -1
  15. fastmcp/client/mixins/__init__.py +13 -0
  16. fastmcp/client/mixins/prompts.py +295 -0
  17. fastmcp/client/mixins/resources.py +325 -0
  18. fastmcp/client/mixins/task_management.py +157 -0
  19. fastmcp/client/mixins/tools.py +397 -0
  20. fastmcp/client/sampling/handlers/anthropic.py +2 -2
  21. fastmcp/client/sampling/handlers/openai.py +1 -1
  22. fastmcp/client/tasks.py +3 -3
  23. fastmcp/client/telemetry.py +47 -0
  24. fastmcp/client/transports/__init__.py +38 -0
  25. fastmcp/client/transports/base.py +82 -0
  26. fastmcp/client/transports/config.py +170 -0
  27. fastmcp/client/transports/http.py +145 -0
  28. fastmcp/client/transports/inference.py +154 -0
  29. fastmcp/client/transports/memory.py +90 -0
  30. fastmcp/client/transports/sse.py +89 -0
  31. fastmcp/client/transports/stdio.py +543 -0
  32. fastmcp/contrib/component_manager/README.md +4 -10
  33. fastmcp/contrib/component_manager/__init__.py +1 -2
  34. fastmcp/contrib/component_manager/component_manager.py +95 -160
  35. fastmcp/contrib/component_manager/example.py +1 -1
  36. fastmcp/contrib/mcp_mixin/example.py +4 -4
  37. fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
  38. fastmcp/decorators.py +41 -0
  39. fastmcp/dependencies.py +12 -1
  40. fastmcp/exceptions.py +4 -0
  41. fastmcp/experimental/server/openapi/__init__.py +18 -15
  42. fastmcp/mcp_config.py +13 -4
  43. fastmcp/prompts/__init__.py +6 -3
  44. fastmcp/prompts/function_prompt.py +465 -0
  45. fastmcp/prompts/prompt.py +321 -271
  46. fastmcp/resources/__init__.py +5 -3
  47. fastmcp/resources/function_resource.py +335 -0
  48. fastmcp/resources/resource.py +325 -115
  49. fastmcp/resources/template.py +215 -43
  50. fastmcp/resources/types.py +27 -12
  51. fastmcp/server/__init__.py +2 -2
  52. fastmcp/server/auth/__init__.py +14 -0
  53. fastmcp/server/auth/auth.py +30 -10
  54. fastmcp/server/auth/authorization.py +190 -0
  55. fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
  56. fastmcp/server/auth/oauth_proxy/consent.py +361 -0
  57. fastmcp/server/auth/oauth_proxy/models.py +178 -0
  58. fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
  59. fastmcp/server/auth/oauth_proxy/ui.py +277 -0
  60. fastmcp/server/auth/oidc_proxy.py +2 -2
  61. fastmcp/server/auth/providers/auth0.py +24 -94
  62. fastmcp/server/auth/providers/aws.py +26 -95
  63. fastmcp/server/auth/providers/azure.py +41 -129
  64. fastmcp/server/auth/providers/descope.py +18 -49
  65. fastmcp/server/auth/providers/discord.py +25 -86
  66. fastmcp/server/auth/providers/github.py +23 -87
  67. fastmcp/server/auth/providers/google.py +24 -87
  68. fastmcp/server/auth/providers/introspection.py +60 -79
  69. fastmcp/server/auth/providers/jwt.py +30 -67
  70. fastmcp/server/auth/providers/oci.py +47 -110
  71. fastmcp/server/auth/providers/scalekit.py +23 -61
  72. fastmcp/server/auth/providers/supabase.py +18 -47
  73. fastmcp/server/auth/providers/workos.py +34 -127
  74. fastmcp/server/context.py +372 -419
  75. fastmcp/server/dependencies.py +541 -251
  76. fastmcp/server/elicitation.py +20 -18
  77. fastmcp/server/event_store.py +3 -3
  78. fastmcp/server/http.py +16 -6
  79. fastmcp/server/lifespan.py +198 -0
  80. fastmcp/server/low_level.py +92 -2
  81. fastmcp/server/middleware/__init__.py +5 -1
  82. fastmcp/server/middleware/authorization.py +312 -0
  83. fastmcp/server/middleware/caching.py +101 -54
  84. fastmcp/server/middleware/middleware.py +6 -9
  85. fastmcp/server/middleware/ping.py +70 -0
  86. fastmcp/server/middleware/tool_injection.py +2 -2
  87. fastmcp/server/mixins/__init__.py +7 -0
  88. fastmcp/server/mixins/lifespan.py +217 -0
  89. fastmcp/server/mixins/mcp_operations.py +392 -0
  90. fastmcp/server/mixins/transport.py +342 -0
  91. fastmcp/server/openapi/__init__.py +41 -21
  92. fastmcp/server/openapi/components.py +16 -339
  93. fastmcp/server/openapi/routing.py +34 -118
  94. fastmcp/server/openapi/server.py +67 -392
  95. fastmcp/server/providers/__init__.py +71 -0
  96. fastmcp/server/providers/aggregate.py +261 -0
  97. fastmcp/server/providers/base.py +578 -0
  98. fastmcp/server/providers/fastmcp_provider.py +674 -0
  99. fastmcp/server/providers/filesystem.py +226 -0
  100. fastmcp/server/providers/filesystem_discovery.py +327 -0
  101. fastmcp/server/providers/local_provider/__init__.py +11 -0
  102. fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
  103. fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
  104. fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
  105. fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
  106. fastmcp/server/providers/local_provider/local_provider.py +465 -0
  107. fastmcp/server/providers/openapi/__init__.py +39 -0
  108. fastmcp/server/providers/openapi/components.py +332 -0
  109. fastmcp/server/providers/openapi/provider.py +405 -0
  110. fastmcp/server/providers/openapi/routing.py +109 -0
  111. fastmcp/server/providers/proxy.py +867 -0
  112. fastmcp/server/providers/skills/__init__.py +59 -0
  113. fastmcp/server/providers/skills/_common.py +101 -0
  114. fastmcp/server/providers/skills/claude_provider.py +44 -0
  115. fastmcp/server/providers/skills/directory_provider.py +153 -0
  116. fastmcp/server/providers/skills/skill_provider.py +432 -0
  117. fastmcp/server/providers/skills/vendor_providers.py +142 -0
  118. fastmcp/server/providers/wrapped_provider.py +140 -0
  119. fastmcp/server/proxy.py +34 -700
  120. fastmcp/server/sampling/run.py +341 -2
  121. fastmcp/server/sampling/sampling_tool.py +4 -3
  122. fastmcp/server/server.py +1214 -2171
  123. fastmcp/server/tasks/__init__.py +2 -1
  124. fastmcp/server/tasks/capabilities.py +13 -1
  125. fastmcp/server/tasks/config.py +66 -3
  126. fastmcp/server/tasks/handlers.py +65 -273
  127. fastmcp/server/tasks/keys.py +4 -6
  128. fastmcp/server/tasks/requests.py +474 -0
  129. fastmcp/server/tasks/routing.py +76 -0
  130. fastmcp/server/tasks/subscriptions.py +20 -11
  131. fastmcp/server/telemetry.py +131 -0
  132. fastmcp/server/transforms/__init__.py +244 -0
  133. fastmcp/server/transforms/namespace.py +193 -0
  134. fastmcp/server/transforms/prompts_as_tools.py +175 -0
  135. fastmcp/server/transforms/resources_as_tools.py +190 -0
  136. fastmcp/server/transforms/tool_transform.py +96 -0
  137. fastmcp/server/transforms/version_filter.py +124 -0
  138. fastmcp/server/transforms/visibility.py +526 -0
  139. fastmcp/settings.py +34 -96
  140. fastmcp/telemetry.py +122 -0
  141. fastmcp/tools/__init__.py +10 -3
  142. fastmcp/tools/function_parsing.py +201 -0
  143. fastmcp/tools/function_tool.py +467 -0
  144. fastmcp/tools/tool.py +215 -362
  145. fastmcp/tools/tool_transform.py +38 -21
  146. fastmcp/utilities/async_utils.py +69 -0
  147. fastmcp/utilities/components.py +152 -91
  148. fastmcp/utilities/inspect.py +8 -20
  149. fastmcp/utilities/json_schema.py +12 -5
  150. fastmcp/utilities/json_schema_type.py +17 -15
  151. fastmcp/utilities/lifespan.py +56 -0
  152. fastmcp/utilities/logging.py +12 -4
  153. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  154. fastmcp/utilities/openapi/parser.py +3 -3
  155. fastmcp/utilities/pagination.py +80 -0
  156. fastmcp/utilities/skills.py +253 -0
  157. fastmcp/utilities/tests.py +0 -16
  158. fastmcp/utilities/timeout.py +47 -0
  159. fastmcp/utilities/types.py +1 -1
  160. fastmcp/utilities/versions.py +285 -0
  161. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
  162. fastmcp-3.0.0b1.dist-info/RECORD +228 -0
  163. fastmcp/client/transports.py +0 -1170
  164. fastmcp/contrib/component_manager/component_service.py +0 -209
  165. fastmcp/prompts/prompt_manager.py +0 -117
  166. fastmcp/resources/resource_manager.py +0 -338
  167. fastmcp/server/tasks/converters.py +0 -206
  168. fastmcp/server/tasks/protocol.py +0 -359
  169. fastmcp/tools/tool_manager.py +0 -170
  170. fastmcp/utilities/mcp_config.py +0 -56
  171. fastmcp-2.14.4.dist-info/RECORD +0 -161
  172. /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
  173. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
  174. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
  175. {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -1,1170 +0,0 @@
1
- import abc
2
- import asyncio
3
- import contextlib
4
- import datetime
5
- import os
6
- import shutil
7
- import sys
8
- import warnings
9
- from collections.abc import AsyncIterator, Callable
10
- from pathlib import Path
11
- from typing import Any, Literal, TextIO, TypeVar, cast, overload
12
-
13
- import anyio
14
- import httpx
15
- import mcp.types
16
- from mcp import ClientSession, StdioServerParameters
17
- from mcp.client.session import (
18
- ElicitationFnT,
19
- ListRootsFnT,
20
- LoggingFnT,
21
- MessageHandlerFnT,
22
- SamplingFnT,
23
- )
24
- from mcp.client.sse import sse_client
25
- from mcp.client.stdio import stdio_client
26
- from mcp.client.streamable_http import streamable_http_client
27
- from mcp.server.fastmcp import FastMCP as FastMCP1Server
28
- from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
29
- from mcp.shared.memory import create_client_server_memory_streams
30
- from pydantic import AnyUrl
31
- from typing_extensions import TypedDict, Unpack
32
-
33
- import fastmcp
34
- from fastmcp.client.auth.bearer import BearerAuth
35
- from fastmcp.client.auth.oauth import OAuth
36
- from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
37
- from fastmcp.server.dependencies import get_http_headers
38
- from fastmcp.server.server import FastMCP
39
- from fastmcp.utilities.logging import get_logger
40
- from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
41
-
42
- logger = get_logger(__name__)
43
-
44
- # TypeVar for preserving specific ClientTransport subclass types
45
- ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
46
-
47
- __all__ = [
48
- "ClientTransport",
49
- "FastMCPStdioTransport",
50
- "FastMCPTransport",
51
- "NodeStdioTransport",
52
- "NpxStdioTransport",
53
- "PythonStdioTransport",
54
- "SSETransport",
55
- "StdioTransport",
56
- "StreamableHttpTransport",
57
- "UvStdioTransport",
58
- "UvxStdioTransport",
59
- "infer_transport",
60
- ]
61
-
62
-
63
- class SessionKwargs(TypedDict, total=False):
64
- """Keyword arguments for the MCP ClientSession constructor."""
65
-
66
- read_timeout_seconds: datetime.timedelta | None
67
- sampling_callback: SamplingFnT | None
68
- sampling_capabilities: mcp.types.SamplingCapability | None
69
- list_roots_callback: ListRootsFnT | None
70
- logging_callback: LoggingFnT | None
71
- elicitation_callback: ElicitationFnT | None
72
- message_handler: MessageHandlerFnT | None
73
- client_info: mcp.types.Implementation | None
74
-
75
-
76
- class ClientTransport(abc.ABC):
77
- """
78
- Abstract base class for different MCP client transport mechanisms.
79
-
80
- A Transport is responsible for establishing and managing connections
81
- to an MCP server, and providing a ClientSession within an async context.
82
-
83
- """
84
-
85
- @abc.abstractmethod
86
- @contextlib.asynccontextmanager
87
- async def connect_session(
88
- self, **session_kwargs: Unpack[SessionKwargs]
89
- ) -> AsyncIterator[ClientSession]:
90
- """
91
- Establishes a connection and yields an active ClientSession.
92
-
93
- The ClientSession is *not* expected to be initialized in this context manager.
94
-
95
- The session is guaranteed to be valid only within the scope of the
96
- async context manager. Connection setup and teardown are handled
97
- within this context.
98
-
99
- Args:
100
- **session_kwargs: Keyword arguments to pass to the ClientSession
101
- constructor (e.g., callbacks, timeouts).
102
-
103
- Yields:
104
- A mcp.ClientSession instance.
105
- """
106
- raise NotImplementedError
107
- yield # type: ignore
108
-
109
- def __repr__(self) -> str:
110
- # Basic representation for subclasses
111
- return f"<{self.__class__.__name__}>"
112
-
113
- async def close(self): # noqa: B027
114
- """Close the transport."""
115
-
116
- def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
117
- if auth is not None:
118
- raise ValueError("This transport does not support auth")
119
-
120
-
121
- class WSTransport(ClientTransport):
122
- """Transport implementation that connects to an MCP server via WebSockets."""
123
-
124
- def __init__(self, url: str | AnyUrl):
125
- # we never really used this transport, so it can be removed at any time
126
- if fastmcp.settings.deprecation_warnings:
127
- warnings.warn(
128
- "WSTransport is a deprecated MCP transport and will be removed in a future version. Use StreamableHttpTransport instead.",
129
- DeprecationWarning,
130
- stacklevel=2,
131
- )
132
- if isinstance(url, AnyUrl):
133
- url = str(url)
134
- if not isinstance(url, str) or not url.startswith("ws"):
135
- raise ValueError("Invalid WebSocket URL provided.")
136
- self.url = url
137
-
138
- @contextlib.asynccontextmanager
139
- async def connect_session(
140
- self, **session_kwargs: Unpack[SessionKwargs]
141
- ) -> AsyncIterator[ClientSession]:
142
- try:
143
- from mcp.client.websocket import websocket_client
144
- except ImportError as e:
145
- raise ImportError(
146
- "The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
147
- ) from e
148
-
149
- async with websocket_client(self.url) as transport:
150
- read_stream, write_stream = transport
151
- async with ClientSession(
152
- read_stream, write_stream, **session_kwargs
153
- ) as session:
154
- yield session
155
-
156
- def __repr__(self) -> str:
157
- return f"<WebSocketTransport(url='{self.url}')>"
158
-
159
-
160
- class SSETransport(ClientTransport):
161
- """Transport implementation that connects to an MCP server via Server-Sent Events."""
162
-
163
- def __init__(
164
- self,
165
- url: str | AnyUrl,
166
- headers: dict[str, str] | None = None,
167
- auth: httpx.Auth | Literal["oauth"] | str | None = None,
168
- sse_read_timeout: datetime.timedelta | float | int | None = None,
169
- httpx_client_factory: McpHttpClientFactory | None = None,
170
- ):
171
- if isinstance(url, AnyUrl):
172
- url = str(url)
173
- if not isinstance(url, str) or not url.startswith("http"):
174
- raise ValueError("Invalid HTTP/S URL provided for SSE.")
175
-
176
- # Don't modify the URL path - respect the exact URL provided by the user
177
- # Some servers are strict about trailing slashes (e.g., PayPal MCP)
178
-
179
- self.url = url
180
- self.headers = headers or {}
181
- self.httpx_client_factory = httpx_client_factory
182
- self._set_auth(auth)
183
-
184
- if isinstance(sse_read_timeout, int | float):
185
- sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
186
- self.sse_read_timeout = sse_read_timeout
187
-
188
- def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
189
- if auth == "oauth":
190
- auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
191
- elif isinstance(auth, str):
192
- auth = BearerAuth(auth)
193
- self.auth = auth
194
-
195
- @contextlib.asynccontextmanager
196
- async def connect_session(
197
- self, **session_kwargs: Unpack[SessionKwargs]
198
- ) -> AsyncIterator[ClientSession]:
199
- client_kwargs: dict[str, Any] = {}
200
-
201
- # load headers from an active HTTP request, if available. This will only be true
202
- # if the client is used in a FastMCP Proxy, in which case the MCP client headers
203
- # need to be forwarded to the remote server.
204
- client_kwargs["headers"] = get_http_headers() | self.headers
205
-
206
- # sse_read_timeout has a default value set, so we can't pass None without overriding it
207
- # instead we simply leave the kwarg out if it's not provided
208
- if self.sse_read_timeout is not None:
209
- client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
210
- if session_kwargs.get("read_timeout_seconds") is not None:
211
- read_timeout_seconds = cast(
212
- datetime.timedelta, session_kwargs.get("read_timeout_seconds")
213
- )
214
- client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
215
-
216
- if self.httpx_client_factory is not None:
217
- client_kwargs["httpx_client_factory"] = self.httpx_client_factory
218
-
219
- async with sse_client(self.url, auth=self.auth, **client_kwargs) as transport:
220
- read_stream, write_stream = transport
221
- async with ClientSession(
222
- read_stream, write_stream, **session_kwargs
223
- ) as session:
224
- yield session
225
-
226
- def __repr__(self) -> str:
227
- return f"<SSETransport(url='{self.url}')>"
228
-
229
-
230
- class StreamableHttpTransport(ClientTransport):
231
- """Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
232
-
233
- def __init__(
234
- self,
235
- url: str | AnyUrl,
236
- headers: dict[str, str] | None = None,
237
- auth: httpx.Auth | Literal["oauth"] | str | None = None,
238
- sse_read_timeout: datetime.timedelta | float | int | None = None,
239
- httpx_client_factory: McpHttpClientFactory | None = None,
240
- ):
241
- if isinstance(url, AnyUrl):
242
- url = str(url)
243
- if not isinstance(url, str) or not url.startswith("http"):
244
- raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.")
245
-
246
- # Don't modify the URL path - respect the exact URL provided by the user
247
- # Some servers are strict about trailing slashes (e.g., PayPal MCP)
248
-
249
- self.url = url
250
- self.headers = headers or {}
251
- self.httpx_client_factory = httpx_client_factory
252
- self._set_auth(auth)
253
-
254
- if sse_read_timeout is not None:
255
- if fastmcp.settings.deprecation_warnings:
256
- warnings.warn(
257
- "The `sse_read_timeout` parameter is deprecated and no longer used. "
258
- "The new streamable_http_client API does not support this parameter. "
259
- "Use `read_timeout_seconds` in session_kwargs or configure timeout on "
260
- "the httpx client via `httpx_client_factory` instead.",
261
- DeprecationWarning,
262
- stacklevel=2,
263
- )
264
- if isinstance(sse_read_timeout, int | float):
265
- sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
266
- self.sse_read_timeout = sse_read_timeout
267
-
268
- self._get_session_id_cb: Callable[[], str | None] | None = None
269
-
270
- def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
271
- if auth == "oauth":
272
- auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
273
- elif isinstance(auth, str):
274
- auth = BearerAuth(auth)
275
- self.auth = auth
276
-
277
- @contextlib.asynccontextmanager
278
- async def connect_session(
279
- self, **session_kwargs: Unpack[SessionKwargs]
280
- ) -> AsyncIterator[ClientSession]:
281
- # Load headers from an active HTTP request, if available. This will only be true
282
- # if the client is used in a FastMCP Proxy, in which case the MCP client headers
283
- # need to be forwarded to the remote server.
284
- headers = get_http_headers() | self.headers
285
-
286
- # Configure timeout if provided, preserving MCP's 30s connect default
287
- timeout: httpx.Timeout | None = None
288
- if session_kwargs.get("read_timeout_seconds") is not None:
289
- read_timeout_seconds = cast(
290
- datetime.timedelta, session_kwargs.get("read_timeout_seconds")
291
- )
292
- timeout = httpx.Timeout(30.0, read=read_timeout_seconds.total_seconds())
293
-
294
- # Create httpx client from factory or use default with MCP-appropriate timeouts
295
- # create_mcp_http_client uses 30s connect/5min read timeout by default,
296
- # and always enables follow_redirects
297
- if self.httpx_client_factory is not None:
298
- # Factory clients get the full kwargs for backwards compatibility
299
- http_client = self.httpx_client_factory(
300
- headers=headers,
301
- auth=self.auth,
302
- follow_redirects=True,
303
- **({"timeout": timeout} if timeout else {}),
304
- )
305
- else:
306
- http_client = create_mcp_http_client(
307
- headers=headers,
308
- timeout=timeout,
309
- auth=self.auth,
310
- )
311
-
312
- # Ensure httpx client is closed after use
313
- async with (
314
- http_client,
315
- streamable_http_client(self.url, http_client=http_client) as transport,
316
- ):
317
- read_stream, write_stream, get_session_id = transport
318
- self._get_session_id_cb = get_session_id
319
- async with ClientSession(
320
- read_stream, write_stream, **session_kwargs
321
- ) as session:
322
- yield session
323
-
324
- def get_session_id(self) -> str | None:
325
- if self._get_session_id_cb:
326
- try:
327
- return self._get_session_id_cb()
328
- except Exception:
329
- return None
330
- return None
331
-
332
- async def close(self):
333
- # Reset the session id callback
334
- self._get_session_id_cb = None
335
-
336
- def __repr__(self) -> str:
337
- return f"<StreamableHttpTransport(url='{self.url}')>"
338
-
339
-
340
- class StdioTransport(ClientTransport):
341
- """
342
- Base transport for connecting to an MCP server via subprocess with stdio.
343
-
344
- This is a base class that can be subclassed for specific command-based
345
- transports like Python, Node, Uvx, etc.
346
- """
347
-
348
- def __init__(
349
- self,
350
- command: str,
351
- args: list[str],
352
- env: dict[str, str] | None = None,
353
- cwd: str | None = None,
354
- keep_alive: bool | None = None,
355
- log_file: Path | TextIO | None = None,
356
- ):
357
- """
358
- Initialize a Stdio transport.
359
-
360
- Args:
361
- command: The command to run (e.g., "python", "node", "uvx")
362
- args: The arguments to pass to the command
363
- env: Environment variables to set for the subprocess
364
- cwd: Current working directory for the subprocess
365
- keep_alive: Whether to keep the subprocess alive between connections.
366
- Defaults to True. When True, the subprocess remains active
367
- after the connection context exits, allowing reuse in
368
- subsequent connections.
369
- log_file: Optional path or file-like object where subprocess stderr will
370
- be written. Can be a Path or TextIO object. Defaults to sys.stderr
371
- if not provided. When a Path is provided, the file will be created
372
- if it doesn't exist, or appended to if it does. When set, server
373
- errors will be written to this file instead of appearing in the console.
374
- """
375
- self.command = command
376
- self.args = args
377
- self.env = env
378
- self.cwd = cwd
379
- if keep_alive is None:
380
- keep_alive = True
381
- self.keep_alive = keep_alive
382
- self.log_file = log_file
383
-
384
- self._session: ClientSession | None = None
385
- self._connect_task: asyncio.Task | None = None
386
- self._ready_event = anyio.Event()
387
- self._stop_event = anyio.Event()
388
-
389
- @contextlib.asynccontextmanager
390
- async def connect_session(
391
- self, **session_kwargs: Unpack[SessionKwargs]
392
- ) -> AsyncIterator[ClientSession]:
393
- try:
394
- await self.connect(**session_kwargs)
395
- yield cast(ClientSession, self._session)
396
- finally:
397
- if not self.keep_alive:
398
- await self.disconnect()
399
- else:
400
- logger.debug("Stdio transport has keep_alive=True, not disconnecting")
401
-
402
- async def connect(
403
- self, **session_kwargs: Unpack[SessionKwargs]
404
- ) -> ClientSession | None:
405
- if self._connect_task is not None:
406
- return
407
-
408
- session_future: asyncio.Future[ClientSession] = asyncio.Future()
409
-
410
- # start the connection task
411
- self._connect_task = asyncio.create_task(
412
- _stdio_transport_connect_task(
413
- command=self.command,
414
- args=self.args,
415
- env=self.env,
416
- cwd=self.cwd,
417
- log_file=self.log_file,
418
- # TODO(ty): remove when ty supports Unpack[TypedDict] inference
419
- session_kwargs=session_kwargs, # type: ignore[arg-type]
420
- ready_event=self._ready_event,
421
- stop_event=self._stop_event,
422
- session_future=session_future,
423
- )
424
- )
425
-
426
- # wait for the client to be ready before returning
427
- await self._ready_event.wait()
428
-
429
- # Check if connect task completed with an exception (early failure)
430
- if self._connect_task.done():
431
- exception = self._connect_task.exception()
432
- if exception is not None:
433
- raise exception
434
-
435
- self._session = await session_future
436
- return self._session
437
-
438
- async def disconnect(self):
439
- if self._connect_task is None:
440
- return
441
-
442
- # signal the connection task to stop
443
- self._stop_event.set()
444
-
445
- # wait for the connection task to finish cleanly
446
- await self._connect_task
447
-
448
- # reset variables and events for potential future reconnects
449
- self._connect_task = None
450
- self._stop_event = anyio.Event()
451
- self._ready_event = anyio.Event()
452
-
453
- async def close(self):
454
- await self.disconnect()
455
-
456
- def __del__(self):
457
- """Ensure that we send a disconnection signal to the transport task if we are being garbage collected."""
458
- if not self._stop_event.is_set():
459
- self._stop_event.set()
460
-
461
- def __repr__(self) -> str:
462
- return (
463
- f"<{self.__class__.__name__}(command='{self.command}', args={self.args})>"
464
- )
465
-
466
-
467
- async def _stdio_transport_connect_task(
468
- command: str,
469
- args: list[str],
470
- env: dict[str, str] | None,
471
- cwd: str | None,
472
- log_file: Path | TextIO | None,
473
- session_kwargs: SessionKwargs,
474
- ready_event: anyio.Event,
475
- stop_event: anyio.Event,
476
- session_future: asyncio.Future[ClientSession],
477
- ):
478
- """A standalone connection task for a stdio transport. It is not a part of the StdioTransport class
479
- to ensure that the connection task does not hold a reference to the Transport object."""
480
-
481
- try:
482
- async with contextlib.AsyncExitStack() as stack:
483
- try:
484
- server_params = StdioServerParameters(
485
- command=command,
486
- args=args,
487
- env=env,
488
- cwd=cwd,
489
- )
490
- # Handle log_file: Path needs to be opened, TextIO used as-is
491
- if log_file is None:
492
- log_file_handle = sys.stderr
493
- elif isinstance(log_file, Path):
494
- log_file_handle = stack.enter_context(log_file.open("a"))
495
- else:
496
- # Must be TextIO - use it directly
497
- log_file_handle = log_file
498
-
499
- transport = await stack.enter_async_context(
500
- stdio_client(server_params, errlog=log_file_handle)
501
- )
502
- read_stream, write_stream = transport
503
- session_future.set_result(
504
- await stack.enter_async_context(
505
- ClientSession(read_stream, write_stream, **session_kwargs)
506
- )
507
- )
508
-
509
- logger.debug("Stdio transport connected")
510
- ready_event.set()
511
-
512
- # Wait until disconnect is requested (stop_event is set)
513
- await stop_event.wait()
514
- finally:
515
- # Clean up client on exit
516
- logger.debug("Stdio transport disconnected")
517
- except Exception:
518
- # Ensure ready event is set even if connection fails
519
- ready_event.set()
520
- raise
521
-
522
-
523
- class PythonStdioTransport(StdioTransport):
524
- """Transport for running Python scripts."""
525
-
526
- def __init__(
527
- self,
528
- script_path: str | Path,
529
- args: list[str] | None = None,
530
- env: dict[str, str] | None = None,
531
- cwd: str | None = None,
532
- python_cmd: str = sys.executable,
533
- keep_alive: bool | None = None,
534
- log_file: Path | TextIO | None = None,
535
- ):
536
- """
537
- Initialize a Python transport.
538
-
539
- Args:
540
- script_path: Path to the Python script to run
541
- args: Additional arguments to pass to the script
542
- env: Environment variables to set for the subprocess
543
- cwd: Current working directory for the subprocess
544
- python_cmd: Python command to use (default: "python")
545
- keep_alive: Whether to keep the subprocess alive between connections.
546
- Defaults to True. When True, the subprocess remains active
547
- after the connection context exits, allowing reuse in
548
- subsequent connections.
549
- log_file: Optional path or file-like object where subprocess stderr will
550
- be written. Can be a Path or TextIO object. Defaults to sys.stderr
551
- if not provided. When a Path is provided, the file will be created
552
- if it doesn't exist, or appended to if it does. When set, server
553
- errors will be written to this file instead of appearing in the console.
554
- """
555
- script_path = Path(script_path).resolve()
556
- if not script_path.is_file():
557
- raise FileNotFoundError(f"Script not found: {script_path}")
558
- if not str(script_path).endswith(".py"):
559
- raise ValueError(f"Not a Python script: {script_path}")
560
-
561
- full_args = [str(script_path)]
562
- if args:
563
- full_args.extend(args)
564
-
565
- super().__init__(
566
- command=python_cmd,
567
- args=full_args,
568
- env=env,
569
- cwd=cwd,
570
- keep_alive=keep_alive,
571
- log_file=log_file,
572
- )
573
- self.script_path = script_path
574
-
575
-
576
- class FastMCPStdioTransport(StdioTransport):
577
- """Transport for running FastMCP servers using the FastMCP CLI."""
578
-
579
- def __init__(
580
- self,
581
- script_path: str | Path,
582
- args: list[str] | None = None,
583
- env: dict[str, str] | None = None,
584
- cwd: str | None = None,
585
- keep_alive: bool | None = None,
586
- log_file: Path | TextIO | None = None,
587
- ):
588
- script_path = Path(script_path).resolve()
589
- if not script_path.is_file():
590
- raise FileNotFoundError(f"Script not found: {script_path}")
591
- if not str(script_path).endswith(".py"):
592
- raise ValueError(f"Not a Python script: {script_path}")
593
-
594
- super().__init__(
595
- command="fastmcp",
596
- args=["run", str(script_path)],
597
- env=env,
598
- cwd=cwd,
599
- keep_alive=keep_alive,
600
- log_file=log_file,
601
- )
602
- self.script_path = script_path
603
-
604
-
605
- class NodeStdioTransport(StdioTransport):
606
- """Transport for running Node.js scripts."""
607
-
608
- def __init__(
609
- self,
610
- script_path: str | Path,
611
- args: list[str] | None = None,
612
- env: dict[str, str] | None = None,
613
- cwd: str | None = None,
614
- node_cmd: str = "node",
615
- keep_alive: bool | None = None,
616
- log_file: Path | TextIO | None = None,
617
- ):
618
- """
619
- Initialize a Node transport.
620
-
621
- Args:
622
- script_path: Path to the Node.js script to run
623
- args: Additional arguments to pass to the script
624
- env: Environment variables to set for the subprocess
625
- cwd: Current working directory for the subprocess
626
- node_cmd: Node.js command to use (default: "node")
627
- keep_alive: Whether to keep the subprocess alive between connections.
628
- Defaults to True. When True, the subprocess remains active
629
- after the connection context exits, allowing reuse in
630
- subsequent connections.
631
- log_file: Optional path or file-like object where subprocess stderr will
632
- be written. Can be a Path or TextIO object. Defaults to sys.stderr
633
- if not provided. When a Path is provided, the file will be created
634
- if it doesn't exist, or appended to if it does. When set, server
635
- errors will be written to this file instead of appearing in the console.
636
- """
637
- script_path = Path(script_path).resolve()
638
- if not script_path.is_file():
639
- raise FileNotFoundError(f"Script not found: {script_path}")
640
- if not str(script_path).endswith(".js"):
641
- raise ValueError(f"Not a JavaScript script: {script_path}")
642
-
643
- full_args = [str(script_path)]
644
- if args:
645
- full_args.extend(args)
646
-
647
- super().__init__(
648
- command=node_cmd,
649
- args=full_args,
650
- env=env,
651
- cwd=cwd,
652
- keep_alive=keep_alive,
653
- log_file=log_file,
654
- )
655
- self.script_path = script_path
656
-
657
-
658
- class UvStdioTransport(StdioTransport):
659
- """Transport for running commands via the uv tool."""
660
-
661
- def __init__(
662
- self,
663
- command: str,
664
- args: list[str] | None = None,
665
- module: bool = False,
666
- project_directory: Path | None = None,
667
- python_version: str | None = None,
668
- with_packages: list[str] | None = None,
669
- with_requirements: Path | None = None,
670
- env_vars: dict[str, str] | None = None,
671
- keep_alive: bool | None = None,
672
- ):
673
- # Basic validation
674
- if project_directory and not project_directory.exists():
675
- raise NotADirectoryError(
676
- f"Project directory not found: {project_directory}"
677
- )
678
-
679
- # Create Environment from provided parameters (internal use)
680
- env_config = UVEnvironment(
681
- python=python_version,
682
- dependencies=with_packages,
683
- requirements=with_requirements,
684
- project=project_directory,
685
- editable=None, # Not exposed in this transport
686
- )
687
-
688
- # Build uv arguments using the config
689
- uv_args: list[str] = []
690
-
691
- # Check if we need any environment setup
692
- if env_config._must_run_with_uv():
693
- # Use the config to build args, but we need to handle the command differently
694
- # since transport has specific needs
695
- uv_args = ["run"]
696
-
697
- if python_version:
698
- uv_args.extend(["--python", python_version])
699
- if project_directory:
700
- uv_args.extend(["--directory", str(project_directory)])
701
-
702
- # Note: Don't add fastmcp as dependency here, transport is for general use
703
- for pkg in with_packages or []:
704
- uv_args.extend(["--with", pkg])
705
- if with_requirements:
706
- uv_args.extend(["--with-requirements", str(with_requirements)])
707
- else:
708
- # No environment setup needed
709
- uv_args = ["run"]
710
-
711
- if module:
712
- uv_args.append("--module")
713
-
714
- if not args:
715
- args = []
716
-
717
- uv_args.extend([command, *args])
718
-
719
- # Get environment with any additional variables
720
- env: dict[str, str] | None = None
721
- if env_vars or project_directory:
722
- env = os.environ.copy()
723
- if project_directory:
724
- env["UV_PROJECT_DIR"] = str(project_directory)
725
- if env_vars:
726
- env.update(env_vars)
727
-
728
- super().__init__(
729
- command="uv",
730
- args=uv_args,
731
- env=env,
732
- cwd=None, # Use --directory flag instead of cwd
733
- keep_alive=keep_alive,
734
- )
735
-
736
-
737
- class UvxStdioTransport(StdioTransport):
738
- """Transport for running commands via the uvx tool."""
739
-
740
- def __init__(
741
- self,
742
- tool_name: str,
743
- tool_args: list[str] | None = None,
744
- project_directory: str | None = None,
745
- python_version: str | None = None,
746
- with_packages: list[str] | None = None,
747
- from_package: str | None = None,
748
- env_vars: dict[str, str] | None = None,
749
- keep_alive: bool | None = None,
750
- ):
751
- """
752
- Initialize a Uvx transport.
753
-
754
- Args:
755
- tool_name: Name of the tool to run via uvx
756
- tool_args: Arguments to pass to the tool
757
- project_directory: Project directory (for package resolution)
758
- python_version: Python version to use
759
- with_packages: Additional packages to include
760
- from_package: Package to install the tool from
761
- env_vars: Additional environment variables
762
- keep_alive: Whether to keep the subprocess alive between connections.
763
- Defaults to True. When True, the subprocess remains active
764
- after the connection context exits, allowing reuse in
765
- subsequent connections.
766
- """
767
- # Basic validation
768
- if project_directory and not Path(project_directory).exists():
769
- raise NotADirectoryError(
770
- f"Project directory not found: {project_directory}"
771
- )
772
-
773
- # Build uvx arguments
774
- uvx_args: list[str] = []
775
- if python_version:
776
- uvx_args.extend(["--python", python_version])
777
- if from_package:
778
- uvx_args.extend(["--from", from_package])
779
- for pkg in with_packages or []:
780
- uvx_args.extend(["--with", pkg])
781
-
782
- # Add the tool name and tool args
783
- uvx_args.append(tool_name)
784
- if tool_args:
785
- uvx_args.extend(tool_args)
786
-
787
- env: dict[str, str] | None = None
788
- if env_vars:
789
- env = os.environ.copy()
790
- env.update(env_vars)
791
-
792
- super().__init__(
793
- command="uvx",
794
- args=uvx_args,
795
- env=env,
796
- cwd=project_directory,
797
- keep_alive=keep_alive,
798
- )
799
- self.tool_name: str = tool_name
800
-
801
-
802
- class NpxStdioTransport(StdioTransport):
803
- """Transport for running commands via the npx tool."""
804
-
805
- def __init__(
806
- self,
807
- package: str,
808
- args: list[str] | None = None,
809
- project_directory: str | None = None,
810
- env_vars: dict[str, str] | None = None,
811
- use_package_lock: bool = True,
812
- keep_alive: bool | None = None,
813
- ):
814
- """
815
- Initialize an Npx transport.
816
-
817
- Args:
818
- package: Name of the npm package to run
819
- args: Arguments to pass to the package command
820
- project_directory: Project directory with package.json
821
- env_vars: Additional environment variables
822
- use_package_lock: Whether to use package-lock.json (--prefer-offline)
823
- keep_alive: Whether to keep the subprocess alive between connections.
824
- Defaults to True. When True, the subprocess remains active
825
- after the connection context exits, allowing reuse in
826
- subsequent connections.
827
- """
828
- # verify npx is installed
829
- if shutil.which("npx") is None:
830
- raise ValueError("Command 'npx' not found")
831
-
832
- # Basic validation
833
- if project_directory and not Path(project_directory).exists():
834
- raise NotADirectoryError(
835
- f"Project directory not found: {project_directory}"
836
- )
837
-
838
- # Build npx arguments
839
- npx_args = []
840
- if use_package_lock:
841
- npx_args.append("--prefer-offline")
842
-
843
- # Add the package name and args
844
- npx_args.append(package)
845
- if args:
846
- npx_args.extend(args)
847
-
848
- # Get environment with any additional variables
849
- env = None
850
- if env_vars:
851
- env = os.environ.copy()
852
- env.update(env_vars)
853
-
854
- super().__init__(
855
- command="npx",
856
- args=npx_args,
857
- env=env,
858
- cwd=project_directory,
859
- keep_alive=keep_alive,
860
- )
861
- self.package = package
862
-
863
-
864
- class FastMCPTransport(ClientTransport):
865
- """In-memory transport for FastMCP servers.
866
-
867
- This transport connects directly to a FastMCP server instance in the same
868
- Python process. It works with both FastMCP 2.x servers and FastMCP 1.0
869
- servers from the low-level MCP SDK. This is particularly useful for unit
870
- tests or scenarios where client and server run in the same runtime.
871
- """
872
-
873
- def __init__(self, mcp: FastMCP | FastMCP1Server, raise_exceptions: bool = False):
874
- """Initialize a FastMCPTransport from a FastMCP server instance."""
875
-
876
- # Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
877
- # ``_mcp_server`` attribute pointing to the underlying MCP server
878
- # implementation, so we can treat them identically.
879
- self.server = mcp
880
- self.raise_exceptions = raise_exceptions
881
-
882
- @contextlib.asynccontextmanager
883
- async def connect_session(
884
- self, **session_kwargs: Unpack[SessionKwargs]
885
- ) -> AsyncIterator[ClientSession]:
886
- async with create_client_server_memory_streams() as (
887
- client_streams,
888
- server_streams,
889
- ):
890
- client_read, client_write = client_streams
891
- server_read, server_write = server_streams
892
-
893
- # Capture exceptions to re-raise after task group cleanup.
894
- # anyio task groups can suppress exceptions when cancel_scope.cancel()
895
- # is called during cleanup, so we capture and re-raise manually.
896
- exception_to_raise: BaseException | None = None
897
-
898
- async with (
899
- anyio.create_task_group() as tg,
900
- _enter_server_lifespan(server=self.server),
901
- ):
902
- tg.start_soon(
903
- lambda: self.server._mcp_server.run(
904
- server_read,
905
- server_write,
906
- self.server._mcp_server.create_initialization_options(),
907
- raise_exceptions=self.raise_exceptions,
908
- )
909
- )
910
-
911
- try:
912
- async with ClientSession(
913
- read_stream=client_read,
914
- write_stream=client_write,
915
- **session_kwargs,
916
- ) as client_session:
917
- yield client_session
918
- except BaseException as e:
919
- exception_to_raise = e
920
- finally:
921
- tg.cancel_scope.cancel()
922
-
923
- # Re-raise after task group has exited cleanly
924
- if exception_to_raise is not None:
925
- raise exception_to_raise
926
-
927
- def __repr__(self) -> str:
928
- return f"<FastMCPTransport(server='{self.server.name}')>"
929
-
930
-
931
- @contextlib.asynccontextmanager
932
- async def _enter_server_lifespan(
933
- server: FastMCP | FastMCP1Server,
934
- ) -> AsyncIterator[None]:
935
- """Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers."""
936
- if isinstance(server, FastMCP):
937
- async with server._lifespan_manager():
938
- yield
939
- else:
940
- yield
941
-
942
-
943
- class MCPConfigTransport(ClientTransport):
944
- """Transport for connecting to one or more MCP servers defined in an MCPConfig.
945
-
946
- This transport provides a unified interface to multiple MCP servers defined in an MCPConfig
947
- object or dictionary matching the MCPConfig schema. It supports two key scenarios:
948
-
949
- 1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.
950
- 2. If the MCPConfig contains multiple servers, it creates a composite client by mounting
951
- all servers on a single FastMCP instance, with each server's name, by default, used as its mounting prefix.
952
-
953
- In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`
954
- and resources with the pattern `protocol://{server_name}/path/to/resource`.
955
-
956
- This is particularly useful for creating clients that need to interact with multiple specialized
957
- MCP servers through a single interface, simplifying client code.
958
-
959
- Examples:
960
- ```python
961
- from fastmcp import Client
962
- from fastmcp.utilities.mcp_config import MCPConfig
963
-
964
- # Create a config with multiple servers
965
- config = {
966
- "mcpServers": {
967
- "weather": {
968
- "url": "https://weather-api.example.com/mcp",
969
- "transport": "http"
970
- },
971
- "calendar": {
972
- "url": "https://calendar-api.example.com/mcp",
973
- "transport": "http"
974
- }
975
- }
976
- }
977
-
978
- # Create a client with the config
979
- client = Client(config)
980
-
981
- async with client:
982
- # Access tools with prefixes
983
- weather = await client.call_tool("weather_get_forecast", {"city": "London"})
984
- events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"})
985
-
986
- # Access resources with prefixed URIs
987
- icons = await client.read_resource("weather://weather/icons/sunny")
988
- ```
989
- """
990
-
991
- def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True):
992
- from fastmcp.utilities.mcp_config import mcp_config_to_servers_and_transports
993
-
994
- if isinstance(config, dict):
995
- config = MCPConfig.from_dict(config)
996
- self.config = config
997
-
998
- self._underlying_transports: list[ClientTransport] = []
999
-
1000
- # if there are no servers, raise an error
1001
- if len(self.config.mcpServers) == 0:
1002
- raise ValueError("No MCP servers defined in the config")
1003
-
1004
- # if there's exactly one server, create a client for that server
1005
- elif len(self.config.mcpServers) == 1:
1006
- self.transport = next(iter(self.config.mcpServers.values())).to_transport()
1007
- self._underlying_transports.append(self.transport)
1008
-
1009
- # otherwise create a composite client
1010
- else:
1011
- name = FastMCP.generate_name("MCPRouter")
1012
- self._composite_server = FastMCP[Any](name=name)
1013
-
1014
- for name, server, transport in mcp_config_to_servers_and_transports(
1015
- self.config
1016
- ):
1017
- self._underlying_transports.append(transport)
1018
- self._composite_server.mount(
1019
- server, prefix=name if name_as_prefix else None
1020
- )
1021
-
1022
- self.transport = FastMCPTransport(mcp=self._composite_server)
1023
-
1024
- @contextlib.asynccontextmanager
1025
- async def connect_session(
1026
- self, **session_kwargs: Unpack[SessionKwargs]
1027
- ) -> AsyncIterator[ClientSession]:
1028
- async with self.transport.connect_session(**session_kwargs) as session:
1029
- yield session
1030
-
1031
- async def close(self):
1032
- for transport in self._underlying_transports:
1033
- await transport.close()
1034
-
1035
- def __repr__(self) -> str:
1036
- return f"<MCPConfigTransport(config='{self.config}')>"
1037
-
1038
-
1039
- @overload
1040
- def infer_transport(transport: ClientTransportT) -> ClientTransportT: ...
1041
-
1042
-
1043
- @overload
1044
- def infer_transport(transport: FastMCP) -> FastMCPTransport: ...
1045
-
1046
-
1047
- @overload
1048
- def infer_transport(transport: FastMCP1Server) -> FastMCPTransport: ...
1049
-
1050
-
1051
- @overload
1052
- def infer_transport(transport: MCPConfig) -> MCPConfigTransport: ...
1053
-
1054
-
1055
- @overload
1056
- def infer_transport(transport: dict[str, Any]) -> MCPConfigTransport: ...
1057
-
1058
-
1059
- @overload
1060
- def infer_transport(
1061
- transport: AnyUrl,
1062
- ) -> SSETransport | StreamableHttpTransport: ...
1063
-
1064
-
1065
- @overload
1066
- def infer_transport(
1067
- transport: str,
1068
- ) -> (
1069
- PythonStdioTransport | NodeStdioTransport | SSETransport | StreamableHttpTransport
1070
- ): ...
1071
-
1072
-
1073
- @overload
1074
- def infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTransport: ...
1075
-
1076
-
1077
- def infer_transport(
1078
- transport: ClientTransport
1079
- | FastMCP
1080
- | FastMCP1Server
1081
- | AnyUrl
1082
- | Path
1083
- | MCPConfig
1084
- | dict[str, Any]
1085
- | str,
1086
- ) -> ClientTransport:
1087
- """
1088
- Infer the appropriate transport type from the given transport argument.
1089
-
1090
- This function attempts to infer the correct transport type from the provided
1091
- argument, handling various input types and converting them to the appropriate
1092
- ClientTransport subclass.
1093
-
1094
- The function supports these input types:
1095
- - ClientTransport: Used directly without modification
1096
- - FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport
1097
- - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
1098
- - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
1099
- - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
1100
-
1101
- For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
1102
-
1103
- For MCPConfig with multiple servers, a composite client is created where each server
1104
- is mounted with its name as prefix. This allows accessing tools and resources from multiple
1105
- servers through a single unified client interface, using naming patterns like
1106
- `servername_toolname` for tools and `protocol://servername/path` for resources.
1107
- If the MCPConfig contains only one server, a direct connection is established without prefixing.
1108
-
1109
- Examples:
1110
- ```python
1111
- # Connect to a local Python script
1112
- transport = infer_transport("my_script.py")
1113
-
1114
- # Connect to a remote server via HTTP
1115
- transport = infer_transport("http://example.com/mcp")
1116
-
1117
- # Connect to multiple servers using MCPConfig
1118
- config = {
1119
- "mcpServers": {
1120
- "weather": {"url": "http://weather.example.com/mcp"},
1121
- "calendar": {"url": "http://calendar.example.com/mcp"}
1122
- }
1123
- }
1124
- transport = infer_transport(config)
1125
- ```
1126
- """
1127
-
1128
- # the transport is already a ClientTransport
1129
- if isinstance(transport, ClientTransport):
1130
- return transport
1131
-
1132
- # the transport is a FastMCP server (2.x or 1.0)
1133
- elif isinstance(transport, FastMCP | FastMCP1Server):
1134
- inferred_transport = FastMCPTransport(
1135
- mcp=cast(FastMCP[Any] | FastMCP1Server, transport)
1136
- )
1137
-
1138
- # the transport is a path to a script
1139
- elif isinstance(transport, Path | str) and Path(transport).exists():
1140
- if str(transport).endswith(".py"):
1141
- inferred_transport = PythonStdioTransport(script_path=cast(Path, transport))
1142
- elif str(transport).endswith(".js"):
1143
- inferred_transport = NodeStdioTransport(script_path=cast(Path, transport))
1144
- else:
1145
- raise ValueError(f"Unsupported script type: {transport}")
1146
-
1147
- # the transport is an http(s) URL
1148
- elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
1149
- inferred_transport_type = infer_transport_type_from_url(
1150
- cast(AnyUrl | str, transport)
1151
- )
1152
- if inferred_transport_type == "sse":
1153
- inferred_transport = SSETransport(url=cast(AnyUrl | str, transport))
1154
- else:
1155
- inferred_transport = StreamableHttpTransport(
1156
- url=cast(AnyUrl | str, transport)
1157
- )
1158
-
1159
- # if the transport is a config dict or MCPConfig
1160
- elif isinstance(transport, dict | MCPConfig):
1161
- inferred_transport = MCPConfigTransport(
1162
- config=cast(dict | MCPConfig, transport)
1163
- )
1164
-
1165
- # the transport is an unknown type
1166
- else:
1167
- raise ValueError(f"Could not infer a valid transport from: {transport}")
1168
-
1169
- logger.debug(f"Inferred transport: {inferred_transport}")
1170
- return inferred_transport