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.
- fastmcp/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {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
|