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
@@ -0,0 +1,82 @@
1
+ import abc
2
+ import contextlib
3
+ import datetime
4
+ from collections.abc import AsyncIterator
5
+ from typing import Literal, TypeVar
6
+
7
+ import httpx
8
+ import mcp.types
9
+ from mcp import ClientSession
10
+ from mcp.client.session import (
11
+ ElicitationFnT,
12
+ ListRootsFnT,
13
+ LoggingFnT,
14
+ MessageHandlerFnT,
15
+ SamplingFnT,
16
+ )
17
+ from typing_extensions import TypedDict, Unpack
18
+
19
+ # TypeVar for preserving specific ClientTransport subclass types
20
+ ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
21
+
22
+
23
+ class SessionKwargs(TypedDict, total=False):
24
+ """Keyword arguments for the MCP ClientSession constructor."""
25
+
26
+ read_timeout_seconds: datetime.timedelta | None
27
+ sampling_callback: SamplingFnT | None
28
+ sampling_capabilities: mcp.types.SamplingCapability | None
29
+ list_roots_callback: ListRootsFnT | None
30
+ logging_callback: LoggingFnT | None
31
+ elicitation_callback: ElicitationFnT | None
32
+ message_handler: MessageHandlerFnT | None
33
+ client_info: mcp.types.Implementation | None
34
+
35
+
36
+ class ClientTransport(abc.ABC):
37
+ """
38
+ Abstract base class for different MCP client transport mechanisms.
39
+
40
+ A Transport is responsible for establishing and managing connections
41
+ to an MCP server, and providing a ClientSession within an async context.
42
+
43
+ """
44
+
45
+ @abc.abstractmethod
46
+ @contextlib.asynccontextmanager
47
+ async def connect_session(
48
+ self, **session_kwargs: Unpack[SessionKwargs]
49
+ ) -> AsyncIterator[ClientSession]:
50
+ """
51
+ Establishes a connection and yields an active ClientSession.
52
+
53
+ The ClientSession is *not* expected to be initialized in this context manager.
54
+
55
+ The session is guaranteed to be valid only within the scope of the
56
+ async context manager. Connection setup and teardown are handled
57
+ within this context.
58
+
59
+ Args:
60
+ **session_kwargs: Keyword arguments to pass to the ClientSession
61
+ constructor (e.g., callbacks, timeouts).
62
+
63
+ Yields:
64
+ A mcp.ClientSession instance.
65
+ """
66
+ raise NotImplementedError
67
+ yield
68
+
69
+ def __repr__(self) -> str:
70
+ # Basic representation for subclasses
71
+ return f"<{self.__class__.__name__}>"
72
+
73
+ async def close(self): # noqa: B027
74
+ """Close the transport."""
75
+
76
+ def get_session_id(self) -> str | None:
77
+ """Get the session ID for this transport, if available."""
78
+ return None
79
+
80
+ def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
81
+ if auth is not None:
82
+ raise ValueError("This transport does not support auth")
@@ -0,0 +1,170 @@
1
+ import contextlib
2
+ import datetime
3
+ from collections.abc import AsyncIterator
4
+ from typing import Any
5
+
6
+ from mcp import ClientSession
7
+ from typing_extensions import Unpack
8
+
9
+ from fastmcp.client.transports.base import ClientTransport, SessionKwargs
10
+ from fastmcp.client.transports.memory import FastMCPTransport
11
+ from fastmcp.mcp_config import (
12
+ MCPConfig,
13
+ MCPServerTypes,
14
+ RemoteMCPServer,
15
+ StdioMCPServer,
16
+ TransformingRemoteMCPServer,
17
+ TransformingStdioMCPServer,
18
+ )
19
+ from fastmcp.server.server import FastMCP, create_proxy
20
+
21
+
22
+ class MCPConfigTransport(ClientTransport):
23
+ """Transport for connecting to one or more MCP servers defined in an MCPConfig.
24
+
25
+ This transport provides a unified interface to multiple MCP servers defined in an MCPConfig
26
+ object or dictionary matching the MCPConfig schema. It supports two key scenarios:
27
+
28
+ 1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.
29
+ 2. If the MCPConfig contains multiple servers, it creates a composite client by mounting
30
+ all servers on a single FastMCP instance, with each server's name, by default, used as its mounting prefix.
31
+
32
+ In the multi-server case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`
33
+ and resources with the pattern `protocol://{server_name}/path/to/resource`.
34
+
35
+ This is particularly useful for creating clients that need to interact with multiple specialized
36
+ MCP servers through a single interface, simplifying client code.
37
+
38
+ Examples:
39
+ ```python
40
+ from fastmcp import Client
41
+
42
+ # Create a config with multiple servers
43
+ config = {
44
+ "mcpServers": {
45
+ "weather": {
46
+ "url": "https://weather-api.example.com/mcp",
47
+ "transport": "http"
48
+ },
49
+ "calendar": {
50
+ "url": "https://calendar-api.example.com/mcp",
51
+ "transport": "http"
52
+ }
53
+ }
54
+ }
55
+
56
+ # Create a client with the config
57
+ client = Client(config)
58
+
59
+ async with client:
60
+ # Access tools with prefixes
61
+ weather = await client.call_tool("weather_get_forecast", {"city": "London"})
62
+ events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"})
63
+
64
+ # Access resources with prefixed URIs
65
+ icons = await client.read_resource("weather://weather/icons/sunny")
66
+ ```
67
+ """
68
+
69
+ def __init__(self, config: MCPConfig | dict, name_as_prefix: bool = True):
70
+ if isinstance(config, dict):
71
+ config = MCPConfig.from_dict(config)
72
+ self.config = config
73
+ self.name_as_prefix = name_as_prefix
74
+ self._transports: list[ClientTransport] = []
75
+
76
+ if not self.config.mcpServers:
77
+ raise ValueError("No MCP servers defined in the config")
78
+
79
+ # For single server, create transport eagerly so it can be inspected
80
+ if len(self.config.mcpServers) == 1:
81
+ self.transport = next(iter(self.config.mcpServers.values())).to_transport()
82
+ self._transports.append(self.transport)
83
+
84
+ @contextlib.asynccontextmanager
85
+ async def connect_session(
86
+ self, **session_kwargs: Unpack[SessionKwargs]
87
+ ) -> AsyncIterator[ClientSession]:
88
+ # Single server - delegate directly to pre-created transport
89
+ if len(self.config.mcpServers) == 1:
90
+ async with self.transport.connect_session(**session_kwargs) as session:
91
+ yield session
92
+ return
93
+
94
+ # Multiple servers - create composite with mounted proxies
95
+ # Close any previous transports from prior connections to avoid leaking
96
+ for t in self._transports:
97
+ await t.close()
98
+ self._transports = []
99
+ timeout = session_kwargs.get("read_timeout_seconds")
100
+ composite = FastMCP[Any](name="MCPRouter")
101
+
102
+ try:
103
+ for name, server_config in self.config.mcpServers.items():
104
+ transport, proxy = self._create_proxy(name, server_config, timeout)
105
+ self._transports.append(transport)
106
+ composite.mount(proxy, namespace=name if self.name_as_prefix else None)
107
+ except Exception:
108
+ # Clean up any transports created before the failure
109
+ for t in self._transports:
110
+ await t.close()
111
+ self._transports = []
112
+ raise
113
+
114
+ async with FastMCPTransport(mcp=composite).connect_session(
115
+ **session_kwargs
116
+ ) as session:
117
+ yield session
118
+
119
+ def _create_proxy(
120
+ self,
121
+ name: str,
122
+ config: MCPServerTypes,
123
+ timeout: datetime.timedelta | None,
124
+ ) -> tuple[ClientTransport, FastMCP[Any]]:
125
+ """Create underlying transport and proxy server for a single backend."""
126
+ # Import here to avoid circular dependency
127
+ from fastmcp.server.providers.proxy import ProxyClient
128
+
129
+ tool_transforms = None
130
+ include_tags = None
131
+ exclude_tags = None
132
+
133
+ # Handle transforming servers - call base class to_transport() for underlying transport
134
+ if isinstance(config, TransformingStdioMCPServer):
135
+ transport = StdioMCPServer.to_transport(config)
136
+ tool_transforms = config.tools
137
+ include_tags = config.include_tags
138
+ exclude_tags = config.exclude_tags
139
+ elif isinstance(config, TransformingRemoteMCPServer):
140
+ transport = RemoteMCPServer.to_transport(config)
141
+ tool_transforms = config.tools
142
+ include_tags = config.include_tags
143
+ exclude_tags = config.exclude_tags
144
+ else:
145
+ transport = config.to_transport()
146
+
147
+ client = ProxyClient(transport=transport, timeout=timeout)
148
+ # Create proxy without include_tags/exclude_tags - we'll add them after tool transforms
149
+ proxy = create_proxy(
150
+ client,
151
+ name=f"Proxy-{name}",
152
+ )
153
+ # Add tool transforms FIRST - they may add/modify tags
154
+ if tool_transforms:
155
+ from fastmcp.server.transforms import ToolTransform
156
+
157
+ proxy.add_transform(ToolTransform(tool_transforms))
158
+ # Then add enabled filters - they filter based on tags
159
+ if include_tags:
160
+ proxy.enable(tags=set(include_tags), only=True)
161
+ if exclude_tags:
162
+ proxy.disable(tags=set(exclude_tags))
163
+ return transport, proxy
164
+
165
+ async def close(self):
166
+ for transport in self._transports:
167
+ await transport.close()
168
+
169
+ def __repr__(self) -> str:
170
+ return f"<MCPConfigTransport(config='{self.config}')>"
@@ -0,0 +1,145 @@
1
+ """Streamable HTTP transport for FastMCP Client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import datetime
7
+ from collections.abc import AsyncIterator, Callable
8
+ from typing import Literal, cast
9
+
10
+ import httpx
11
+ from mcp import ClientSession
12
+ from mcp.client.streamable_http import streamable_http_client
13
+ from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
14
+ from pydantic import AnyUrl
15
+ from typing_extensions import Unpack
16
+
17
+ import fastmcp
18
+ from fastmcp.client.auth.bearer import BearerAuth
19
+ from fastmcp.client.auth.oauth import OAuth
20
+ from fastmcp.client.transports.base import ClientTransport, SessionKwargs
21
+ from fastmcp.server.dependencies import get_http_headers
22
+ from fastmcp.utilities.timeout import normalize_timeout_to_timedelta
23
+
24
+
25
+ class StreamableHttpTransport(ClientTransport):
26
+ """Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
27
+
28
+ def __init__(
29
+ self,
30
+ url: str | AnyUrl,
31
+ headers: dict[str, str] | None = None,
32
+ auth: httpx.Auth | Literal["oauth"] | str | None = None,
33
+ sse_read_timeout: datetime.timedelta | float | int | None = None,
34
+ httpx_client_factory: McpHttpClientFactory | None = None,
35
+ ):
36
+ """Initialize a Streamable HTTP transport.
37
+
38
+ Args:
39
+ url: The MCP server endpoint URL.
40
+ headers: Optional headers to include in requests.
41
+ auth: Authentication method - httpx.Auth, "oauth" for OAuth flow,
42
+ or a bearer token string.
43
+ sse_read_timeout: Deprecated. Use read_timeout_seconds in session_kwargs.
44
+ httpx_client_factory: Optional factory for creating httpx.AsyncClient.
45
+ If provided, must accept keyword arguments: headers, auth,
46
+ follow_redirects, and optionally timeout. Using **kwargs is
47
+ recommended to ensure forward compatibility.
48
+ """
49
+ if isinstance(url, AnyUrl):
50
+ url = str(url)
51
+ if not isinstance(url, str) or not url.startswith("http"):
52
+ raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.")
53
+
54
+ # Don't modify the URL path - respect the exact URL provided by the user
55
+ # Some servers are strict about trailing slashes (e.g., PayPal MCP)
56
+
57
+ self.url: str = url
58
+ self.headers = headers or {}
59
+ self.httpx_client_factory = httpx_client_factory
60
+ self._set_auth(auth)
61
+
62
+ if sse_read_timeout is not None:
63
+ if fastmcp.settings.deprecation_warnings:
64
+ import warnings
65
+
66
+ warnings.warn(
67
+ "The `sse_read_timeout` parameter is deprecated and no longer used. "
68
+ "The new streamable_http_client API does not support this parameter. "
69
+ "Use `read_timeout_seconds` in session_kwargs or configure timeout on "
70
+ "the httpx client via `httpx_client_factory` instead.",
71
+ DeprecationWarning,
72
+ stacklevel=2,
73
+ )
74
+ self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout)
75
+
76
+ self._get_session_id_cb: Callable[[], str | None] | None = None
77
+
78
+ def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
79
+ if auth == "oauth":
80
+ auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
81
+ elif isinstance(auth, str):
82
+ auth = BearerAuth(auth)
83
+ self.auth = auth
84
+
85
+ @contextlib.asynccontextmanager
86
+ async def connect_session(
87
+ self, **session_kwargs: Unpack[SessionKwargs]
88
+ ) -> AsyncIterator[ClientSession]:
89
+ # Load headers from an active HTTP request, if available. This will only be true
90
+ # if the client is used in a FastMCP Proxy, in which case the MCP client headers
91
+ # need to be forwarded to the remote server.
92
+ headers = get_http_headers() | self.headers
93
+
94
+ # Configure timeout if provided, preserving MCP's 30s connect default
95
+ timeout: httpx.Timeout | None = None
96
+ if session_kwargs.get("read_timeout_seconds") is not None:
97
+ read_timeout_seconds = cast(
98
+ datetime.timedelta, session_kwargs.get("read_timeout_seconds")
99
+ )
100
+ timeout = httpx.Timeout(30.0, read=read_timeout_seconds.total_seconds())
101
+
102
+ # Create httpx client from factory or use default with MCP-appropriate timeouts
103
+ # create_mcp_http_client uses 30s connect/5min read timeout by default,
104
+ # and always enables follow_redirects
105
+ if self.httpx_client_factory is not None:
106
+ # Factory clients get the full kwargs for backwards compatibility
107
+ http_client = self.httpx_client_factory(
108
+ headers=headers,
109
+ auth=self.auth,
110
+ follow_redirects=True, # type: ignore[call-arg]
111
+ **({"timeout": timeout} if timeout else {}),
112
+ )
113
+ else:
114
+ http_client = create_mcp_http_client(
115
+ headers=headers,
116
+ timeout=timeout,
117
+ auth=self.auth,
118
+ )
119
+
120
+ # Ensure httpx client is closed after use
121
+ async with (
122
+ http_client,
123
+ streamable_http_client(self.url, http_client=http_client) as transport,
124
+ ):
125
+ read_stream, write_stream, get_session_id = transport
126
+ self._get_session_id_cb = get_session_id
127
+ async with ClientSession(
128
+ read_stream, write_stream, **session_kwargs
129
+ ) as session:
130
+ yield session
131
+
132
+ def get_session_id(self) -> str | None:
133
+ if self._get_session_id_cb:
134
+ try:
135
+ return self._get_session_id_cb()
136
+ except Exception:
137
+ return None
138
+ return None
139
+
140
+ async def close(self):
141
+ # Reset the session id callback
142
+ self._get_session_id_cb = None
143
+
144
+ def __repr__(self) -> str:
145
+ return f"<StreamableHttpTransport(url='{self.url}')>"
@@ -0,0 +1,154 @@
1
+ from pathlib import Path
2
+ from typing import TYPE_CHECKING, Any, cast, overload
3
+
4
+ from mcp.server.fastmcp import FastMCP as FastMCP1Server
5
+ from pydantic import AnyUrl
6
+
7
+ from fastmcp.client.transports.base import ClientTransport, ClientTransportT
8
+ from fastmcp.client.transports.config import MCPConfigTransport
9
+ from fastmcp.client.transports.http import StreamableHttpTransport
10
+ from fastmcp.client.transports.memory import FastMCPTransport
11
+ from fastmcp.client.transports.sse import SSETransport
12
+ from fastmcp.client.transports.stdio import NodeStdioTransport, PythonStdioTransport
13
+ from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
14
+ from fastmcp.server.server import FastMCP
15
+ from fastmcp.utilities.logging import get_logger
16
+
17
+ if TYPE_CHECKING:
18
+ pass
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ @overload
24
+ def infer_transport(transport: ClientTransportT) -> ClientTransportT: ...
25
+
26
+
27
+ @overload
28
+ def infer_transport(transport: FastMCP) -> FastMCPTransport: ...
29
+
30
+
31
+ @overload
32
+ def infer_transport(transport: FastMCP1Server) -> FastMCPTransport: ...
33
+
34
+
35
+ @overload
36
+ def infer_transport(transport: MCPConfig) -> MCPConfigTransport: ...
37
+
38
+
39
+ @overload
40
+ def infer_transport(transport: dict[str, Any]) -> MCPConfigTransport: ...
41
+
42
+
43
+ @overload
44
+ def infer_transport(
45
+ transport: AnyUrl,
46
+ ) -> SSETransport | StreamableHttpTransport: ...
47
+
48
+
49
+ @overload
50
+ def infer_transport(
51
+ transport: str,
52
+ ) -> (
53
+ PythonStdioTransport | NodeStdioTransport | SSETransport | StreamableHttpTransport
54
+ ): ...
55
+
56
+
57
+ @overload
58
+ def infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTransport: ...
59
+
60
+
61
+ def infer_transport(
62
+ transport: ClientTransport
63
+ | FastMCP
64
+ | FastMCP1Server
65
+ | AnyUrl
66
+ | Path
67
+ | MCPConfig
68
+ | dict[str, Any]
69
+ | str,
70
+ ) -> ClientTransport:
71
+ """
72
+ Infer the appropriate transport type from the given transport argument.
73
+
74
+ This function attempts to infer the correct transport type from the provided
75
+ argument, handling various input types and converting them to the appropriate
76
+ ClientTransport subclass.
77
+
78
+ The function supports these input types:
79
+ - ClientTransport: Used directly without modification
80
+ - FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport
81
+ - Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
82
+ - AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
83
+ - MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
84
+
85
+ For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
86
+
87
+ For MCPConfig with multiple servers, a composite client is created where each server
88
+ is mounted with its name as prefix. This allows accessing tools and resources from multiple
89
+ servers through a single unified client interface, using naming patterns like
90
+ `servername_toolname` for tools and `protocol://servername/path` for resources.
91
+ If the MCPConfig contains only one server, a direct connection is established without prefixing.
92
+
93
+ Examples:
94
+ ```python
95
+ # Connect to a local Python script
96
+ transport = infer_transport("my_script.py")
97
+
98
+ # Connect to a remote server via HTTP
99
+ transport = infer_transport("http://example.com/mcp")
100
+
101
+ # Connect to multiple servers using MCPConfig
102
+ config = {
103
+ "mcpServers": {
104
+ "weather": {"url": "http://weather.example.com/mcp"},
105
+ "calendar": {"url": "http://calendar.example.com/mcp"}
106
+ }
107
+ }
108
+ transport = infer_transport(config)
109
+ ```
110
+ """
111
+
112
+ # the transport is already a ClientTransport
113
+ if isinstance(transport, ClientTransport):
114
+ return transport
115
+
116
+ # the transport is a FastMCP server (2.x or 1.0)
117
+ elif isinstance(transport, FastMCP | FastMCP1Server):
118
+ inferred_transport = FastMCPTransport(
119
+ mcp=cast(FastMCP[Any] | FastMCP1Server, transport)
120
+ )
121
+
122
+ # the transport is a path to a script
123
+ elif isinstance(transport, Path | str) and Path(transport).exists():
124
+ if str(transport).endswith(".py"):
125
+ inferred_transport = PythonStdioTransport(script_path=cast(Path, transport))
126
+ elif str(transport).endswith(".js"):
127
+ inferred_transport = NodeStdioTransport(script_path=cast(Path, transport))
128
+ else:
129
+ raise ValueError(f"Unsupported script type: {transport}")
130
+
131
+ # the transport is an http(s) URL
132
+ elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
133
+ inferred_transport_type = infer_transport_type_from_url(
134
+ cast(AnyUrl | str, transport)
135
+ )
136
+ if inferred_transport_type == "sse":
137
+ inferred_transport = SSETransport(url=cast(AnyUrl | str, transport))
138
+ else:
139
+ inferred_transport = StreamableHttpTransport(
140
+ url=cast(AnyUrl | str, transport)
141
+ )
142
+
143
+ # if the transport is a config dict or MCPConfig
144
+ elif isinstance(transport, dict | MCPConfig):
145
+ inferred_transport = MCPConfigTransport(
146
+ config=cast(dict | MCPConfig, transport)
147
+ )
148
+
149
+ # the transport is an unknown type
150
+ else:
151
+ raise ValueError(f"Could not infer a valid transport from: {transport}")
152
+
153
+ logger.debug(f"Inferred transport: {inferred_transport}")
154
+ return inferred_transport
@@ -0,0 +1,90 @@
1
+ import contextlib
2
+ from collections.abc import AsyncIterator
3
+
4
+ import anyio
5
+ from mcp import ClientSession
6
+ from mcp.server.fastmcp import FastMCP as FastMCP1Server
7
+ from mcp.shared.memory import create_client_server_memory_streams
8
+ from typing_extensions import Unpack
9
+
10
+ from fastmcp.client.transports.base import ClientTransport, SessionKwargs
11
+ from fastmcp.server.server import FastMCP
12
+
13
+
14
+ class FastMCPTransport(ClientTransport):
15
+ """In-memory transport for FastMCP servers.
16
+
17
+ This transport connects directly to a FastMCP server instance in the same
18
+ Python process. It works with both FastMCP 2.x servers and FastMCP 1.0
19
+ servers from the low-level MCP SDK. This is particularly useful for unit
20
+ tests or scenarios where client and server run in the same runtime.
21
+ """
22
+
23
+ def __init__(self, mcp: FastMCP | FastMCP1Server, raise_exceptions: bool = False):
24
+ """Initialize a FastMCPTransport from a FastMCP server instance."""
25
+
26
+ # Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
27
+ # ``_mcp_server`` attribute pointing to the underlying MCP server
28
+ # implementation, so we can treat them identically.
29
+ self.server = mcp
30
+ self.raise_exceptions = raise_exceptions
31
+
32
+ @contextlib.asynccontextmanager
33
+ async def connect_session(
34
+ self, **session_kwargs: Unpack[SessionKwargs]
35
+ ) -> AsyncIterator[ClientSession]:
36
+ async with create_client_server_memory_streams() as (
37
+ client_streams,
38
+ server_streams,
39
+ ):
40
+ client_read, client_write = client_streams
41
+ server_read, server_write = server_streams
42
+
43
+ # Capture exceptions to re-raise after task group cleanup.
44
+ # anyio task groups can suppress exceptions when cancel_scope.cancel()
45
+ # is called during cleanup, so we capture and re-raise manually.
46
+ exception_to_raise: BaseException | None = None
47
+
48
+ async with (
49
+ anyio.create_task_group() as tg,
50
+ _enter_server_lifespan(server=self.server),
51
+ ):
52
+ tg.start_soon(
53
+ lambda: self.server._mcp_server.run(
54
+ server_read,
55
+ server_write,
56
+ self.server._mcp_server.create_initialization_options(),
57
+ raise_exceptions=self.raise_exceptions,
58
+ )
59
+ )
60
+
61
+ try:
62
+ async with ClientSession(
63
+ read_stream=client_read,
64
+ write_stream=client_write,
65
+ **session_kwargs,
66
+ ) as client_session:
67
+ yield client_session
68
+ except BaseException as e:
69
+ exception_to_raise = e
70
+ finally:
71
+ tg.cancel_scope.cancel()
72
+
73
+ # Re-raise after task group has exited cleanly
74
+ if exception_to_raise is not None:
75
+ raise exception_to_raise
76
+
77
+ def __repr__(self) -> str:
78
+ return f"<FastMCPTransport(server='{self.server.name}')>"
79
+
80
+
81
+ @contextlib.asynccontextmanager
82
+ async def _enter_server_lifespan(
83
+ server: FastMCP | FastMCP1Server,
84
+ ) -> AsyncIterator[None]:
85
+ """Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers."""
86
+ if isinstance(server, FastMCP):
87
+ async with server._lifespan_manager():
88
+ yield
89
+ else:
90
+ yield