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,89 @@
|
|
|
1
|
+
"""Server-Sent Events (SSE) transport for FastMCP Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import datetime
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from typing import Any, Literal, cast
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from mcp import ClientSession
|
|
12
|
+
from mcp.client.sse import sse_client
|
|
13
|
+
from mcp.shared._httpx_utils import McpHttpClientFactory
|
|
14
|
+
from pydantic import AnyUrl
|
|
15
|
+
from typing_extensions import Unpack
|
|
16
|
+
|
|
17
|
+
from fastmcp.client.auth.bearer import BearerAuth
|
|
18
|
+
from fastmcp.client.auth.oauth import OAuth
|
|
19
|
+
from fastmcp.client.transports.base import ClientTransport, SessionKwargs
|
|
20
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
21
|
+
from fastmcp.utilities.timeout import normalize_timeout_to_timedelta
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SSETransport(ClientTransport):
|
|
25
|
+
"""Transport implementation that connects to an MCP server via Server-Sent Events."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
url: str | AnyUrl,
|
|
30
|
+
headers: dict[str, str] | None = None,
|
|
31
|
+
auth: httpx.Auth | Literal["oauth"] | str | None = None,
|
|
32
|
+
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
33
|
+
httpx_client_factory: McpHttpClientFactory | None = None,
|
|
34
|
+
):
|
|
35
|
+
if isinstance(url, AnyUrl):
|
|
36
|
+
url = str(url)
|
|
37
|
+
if not isinstance(url, str) or not url.startswith("http"):
|
|
38
|
+
raise ValueError("Invalid HTTP/S URL provided for SSE.")
|
|
39
|
+
|
|
40
|
+
# Don't modify the URL path - respect the exact URL provided by the user
|
|
41
|
+
# Some servers are strict about trailing slashes (e.g., PayPal MCP)
|
|
42
|
+
|
|
43
|
+
self.url: str = url
|
|
44
|
+
self.headers = headers or {}
|
|
45
|
+
self.httpx_client_factory = httpx_client_factory
|
|
46
|
+
self._set_auth(auth)
|
|
47
|
+
|
|
48
|
+
self.sse_read_timeout = normalize_timeout_to_timedelta(sse_read_timeout)
|
|
49
|
+
|
|
50
|
+
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
51
|
+
if auth == "oauth":
|
|
52
|
+
auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
|
|
53
|
+
elif isinstance(auth, str):
|
|
54
|
+
auth = BearerAuth(auth)
|
|
55
|
+
self.auth = auth
|
|
56
|
+
|
|
57
|
+
@contextlib.asynccontextmanager
|
|
58
|
+
async def connect_session(
|
|
59
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
60
|
+
) -> AsyncIterator[ClientSession]:
|
|
61
|
+
client_kwargs: dict[str, Any] = {}
|
|
62
|
+
|
|
63
|
+
# load headers from an active HTTP request, if available. This will only be true
|
|
64
|
+
# if the client is used in a FastMCP Proxy, in which case the MCP client headers
|
|
65
|
+
# need to be forwarded to the remote server.
|
|
66
|
+
client_kwargs["headers"] = get_http_headers() | self.headers
|
|
67
|
+
|
|
68
|
+
# sse_read_timeout has a default value set, so we can't pass None without overriding it
|
|
69
|
+
# instead we simply leave the kwarg out if it's not provided
|
|
70
|
+
if self.sse_read_timeout is not None:
|
|
71
|
+
client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
|
|
72
|
+
if session_kwargs.get("read_timeout_seconds") is not None:
|
|
73
|
+
read_timeout_seconds = cast(
|
|
74
|
+
datetime.timedelta, session_kwargs.get("read_timeout_seconds")
|
|
75
|
+
)
|
|
76
|
+
client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
|
|
77
|
+
|
|
78
|
+
if self.httpx_client_factory is not None:
|
|
79
|
+
client_kwargs["httpx_client_factory"] = self.httpx_client_factory
|
|
80
|
+
|
|
81
|
+
async with sse_client(self.url, auth=self.auth, **client_kwargs) as transport:
|
|
82
|
+
read_stream, write_stream = transport
|
|
83
|
+
async with ClientSession(
|
|
84
|
+
read_stream, write_stream, **session_kwargs
|
|
85
|
+
) as session:
|
|
86
|
+
yield session
|
|
87
|
+
|
|
88
|
+
def __repr__(self) -> str:
|
|
89
|
+
return f"<SSETransport(url='{self.url}')>"
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TextIO, cast
|
|
9
|
+
|
|
10
|
+
import anyio
|
|
11
|
+
from mcp import ClientSession, StdioServerParameters
|
|
12
|
+
from mcp.client.stdio import stdio_client
|
|
13
|
+
from typing_extensions import Unpack
|
|
14
|
+
|
|
15
|
+
from fastmcp.client.transports.base import ClientTransport, SessionKwargs
|
|
16
|
+
from fastmcp.utilities.logging import get_logger
|
|
17
|
+
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StdioTransport(ClientTransport):
|
|
23
|
+
"""
|
|
24
|
+
Base transport for connecting to an MCP server via subprocess with stdio.
|
|
25
|
+
|
|
26
|
+
This is a base class that can be subclassed for specific command-based
|
|
27
|
+
transports like Python, Node, Uvx, etc.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
command: str,
|
|
33
|
+
args: list[str],
|
|
34
|
+
env: dict[str, str] | None = None,
|
|
35
|
+
cwd: str | None = None,
|
|
36
|
+
keep_alive: bool | None = None,
|
|
37
|
+
log_file: Path | TextIO | None = None,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize a Stdio transport.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
command: The command to run (e.g., "python", "node", "uvx")
|
|
44
|
+
args: The arguments to pass to the command
|
|
45
|
+
env: Environment variables to set for the subprocess
|
|
46
|
+
cwd: Current working directory for the subprocess
|
|
47
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
48
|
+
Defaults to True. When True, the subprocess remains active
|
|
49
|
+
after the connection context exits, allowing reuse in
|
|
50
|
+
subsequent connections.
|
|
51
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
52
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
53
|
+
if not provided. When a Path is provided, the file will be created
|
|
54
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
55
|
+
errors will be written to this file instead of appearing in the console.
|
|
56
|
+
"""
|
|
57
|
+
self.command = command
|
|
58
|
+
self.args = args
|
|
59
|
+
self.env = env
|
|
60
|
+
self.cwd = cwd
|
|
61
|
+
if keep_alive is None:
|
|
62
|
+
keep_alive = True
|
|
63
|
+
self.keep_alive = keep_alive
|
|
64
|
+
self.log_file = log_file
|
|
65
|
+
|
|
66
|
+
self._session: ClientSession | None = None
|
|
67
|
+
self._connect_task: asyncio.Task | None = None
|
|
68
|
+
self._ready_event = anyio.Event()
|
|
69
|
+
self._stop_event = anyio.Event()
|
|
70
|
+
|
|
71
|
+
@contextlib.asynccontextmanager
|
|
72
|
+
async def connect_session(
|
|
73
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
74
|
+
) -> AsyncIterator[ClientSession]:
|
|
75
|
+
try:
|
|
76
|
+
await self.connect(**session_kwargs)
|
|
77
|
+
yield cast(ClientSession, self._session)
|
|
78
|
+
finally:
|
|
79
|
+
if not self.keep_alive:
|
|
80
|
+
await self.disconnect()
|
|
81
|
+
else:
|
|
82
|
+
logger.debug("Stdio transport has keep_alive=True, not disconnecting")
|
|
83
|
+
|
|
84
|
+
async def connect(
|
|
85
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
86
|
+
) -> ClientSession | None:
|
|
87
|
+
if self._connect_task is not None:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
session_future: asyncio.Future[ClientSession] = asyncio.Future()
|
|
91
|
+
|
|
92
|
+
# start the connection task
|
|
93
|
+
self._connect_task = asyncio.create_task(
|
|
94
|
+
_stdio_transport_connect_task(
|
|
95
|
+
command=self.command,
|
|
96
|
+
args=self.args,
|
|
97
|
+
env=self.env,
|
|
98
|
+
cwd=self.cwd,
|
|
99
|
+
log_file=self.log_file,
|
|
100
|
+
# TODO(ty): remove when ty supports Unpack[TypedDict] inference
|
|
101
|
+
session_kwargs=session_kwargs, # type: ignore[arg-type]
|
|
102
|
+
ready_event=self._ready_event,
|
|
103
|
+
stop_event=self._stop_event,
|
|
104
|
+
session_future=session_future,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# wait for the client to be ready before returning
|
|
109
|
+
await self._ready_event.wait()
|
|
110
|
+
|
|
111
|
+
# Check if connect task completed with an exception (early failure)
|
|
112
|
+
if self._connect_task.done():
|
|
113
|
+
exception = self._connect_task.exception()
|
|
114
|
+
if exception is not None:
|
|
115
|
+
raise exception
|
|
116
|
+
|
|
117
|
+
self._session = await session_future
|
|
118
|
+
return self._session
|
|
119
|
+
|
|
120
|
+
async def disconnect(self):
|
|
121
|
+
if self._connect_task is None:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# signal the connection task to stop
|
|
125
|
+
self._stop_event.set()
|
|
126
|
+
|
|
127
|
+
# wait for the connection task to finish cleanly
|
|
128
|
+
await self._connect_task
|
|
129
|
+
|
|
130
|
+
# reset variables and events for potential future reconnects
|
|
131
|
+
self._connect_task = None
|
|
132
|
+
self._stop_event = anyio.Event()
|
|
133
|
+
self._ready_event = anyio.Event()
|
|
134
|
+
|
|
135
|
+
async def close(self):
|
|
136
|
+
await self.disconnect()
|
|
137
|
+
|
|
138
|
+
def __del__(self):
|
|
139
|
+
"""Ensure that we send a disconnection signal to the transport task if we are being garbage collected."""
|
|
140
|
+
if not self._stop_event.is_set():
|
|
141
|
+
self._stop_event.set()
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
return (
|
|
145
|
+
f"<{self.__class__.__name__}(command='{self.command}', args={self.args})>"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _stdio_transport_connect_task(
|
|
150
|
+
command: str,
|
|
151
|
+
args: list[str],
|
|
152
|
+
env: dict[str, str] | None,
|
|
153
|
+
cwd: str | None,
|
|
154
|
+
log_file: Path | TextIO | None,
|
|
155
|
+
session_kwargs: SessionKwargs,
|
|
156
|
+
ready_event: anyio.Event,
|
|
157
|
+
stop_event: anyio.Event,
|
|
158
|
+
session_future: asyncio.Future[ClientSession],
|
|
159
|
+
):
|
|
160
|
+
"""A standalone connection task for a stdio transport. It is not a part of the StdioTransport class
|
|
161
|
+
to ensure that the connection task does not hold a reference to the Transport object."""
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
165
|
+
try:
|
|
166
|
+
server_params = StdioServerParameters(
|
|
167
|
+
command=command,
|
|
168
|
+
args=args,
|
|
169
|
+
env=env,
|
|
170
|
+
cwd=cwd,
|
|
171
|
+
)
|
|
172
|
+
# Handle log_file: Path needs to be opened, TextIO used as-is
|
|
173
|
+
if log_file is None:
|
|
174
|
+
log_file_handle = sys.stderr
|
|
175
|
+
elif isinstance(log_file, Path):
|
|
176
|
+
log_file_handle = stack.enter_context(log_file.open("a"))
|
|
177
|
+
else:
|
|
178
|
+
# Must be TextIO - use it directly
|
|
179
|
+
log_file_handle = log_file
|
|
180
|
+
|
|
181
|
+
transport = await stack.enter_async_context(
|
|
182
|
+
stdio_client(server_params, errlog=log_file_handle)
|
|
183
|
+
)
|
|
184
|
+
read_stream, write_stream = transport
|
|
185
|
+
session_future.set_result(
|
|
186
|
+
await stack.enter_async_context(
|
|
187
|
+
ClientSession(read_stream, write_stream, **session_kwargs)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
logger.debug("Stdio transport connected")
|
|
192
|
+
ready_event.set()
|
|
193
|
+
|
|
194
|
+
# Wait until disconnect is requested (stop_event is set)
|
|
195
|
+
await stop_event.wait()
|
|
196
|
+
finally:
|
|
197
|
+
# Clean up client on exit
|
|
198
|
+
logger.debug("Stdio transport disconnected")
|
|
199
|
+
except Exception:
|
|
200
|
+
# Ensure ready event is set even if connection fails
|
|
201
|
+
ready_event.set()
|
|
202
|
+
raise
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class PythonStdioTransport(StdioTransport):
|
|
206
|
+
"""Transport for running Python scripts."""
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
script_path: str | Path,
|
|
211
|
+
args: list[str] | None = None,
|
|
212
|
+
env: dict[str, str] | None = None,
|
|
213
|
+
cwd: str | None = None,
|
|
214
|
+
python_cmd: str = sys.executable,
|
|
215
|
+
keep_alive: bool | None = None,
|
|
216
|
+
log_file: Path | TextIO | None = None,
|
|
217
|
+
):
|
|
218
|
+
"""
|
|
219
|
+
Initialize a Python transport.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
script_path: Path to the Python script to run
|
|
223
|
+
args: Additional arguments to pass to the script
|
|
224
|
+
env: Environment variables to set for the subprocess
|
|
225
|
+
cwd: Current working directory for the subprocess
|
|
226
|
+
python_cmd: Python command to use (default: "python")
|
|
227
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
228
|
+
Defaults to True. When True, the subprocess remains active
|
|
229
|
+
after the connection context exits, allowing reuse in
|
|
230
|
+
subsequent connections.
|
|
231
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
232
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
233
|
+
if not provided. When a Path is provided, the file will be created
|
|
234
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
235
|
+
errors will be written to this file instead of appearing in the console.
|
|
236
|
+
"""
|
|
237
|
+
script_path = Path(script_path).resolve()
|
|
238
|
+
if not script_path.is_file():
|
|
239
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
240
|
+
if not str(script_path).endswith(".py"):
|
|
241
|
+
raise ValueError(f"Not a Python script: {script_path}")
|
|
242
|
+
|
|
243
|
+
full_args = [str(script_path)]
|
|
244
|
+
if args:
|
|
245
|
+
full_args.extend(args)
|
|
246
|
+
|
|
247
|
+
super().__init__(
|
|
248
|
+
command=python_cmd,
|
|
249
|
+
args=full_args,
|
|
250
|
+
env=env,
|
|
251
|
+
cwd=cwd,
|
|
252
|
+
keep_alive=keep_alive,
|
|
253
|
+
log_file=log_file,
|
|
254
|
+
)
|
|
255
|
+
self.script_path = script_path
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class FastMCPStdioTransport(StdioTransport):
|
|
259
|
+
"""Transport for running FastMCP servers using the FastMCP CLI."""
|
|
260
|
+
|
|
261
|
+
def __init__(
|
|
262
|
+
self,
|
|
263
|
+
script_path: str | Path,
|
|
264
|
+
args: list[str] | None = None,
|
|
265
|
+
env: dict[str, str] | None = None,
|
|
266
|
+
cwd: str | None = None,
|
|
267
|
+
keep_alive: bool | None = None,
|
|
268
|
+
log_file: Path | TextIO | None = None,
|
|
269
|
+
):
|
|
270
|
+
script_path = Path(script_path).resolve()
|
|
271
|
+
if not script_path.is_file():
|
|
272
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
273
|
+
if not str(script_path).endswith(".py"):
|
|
274
|
+
raise ValueError(f"Not a Python script: {script_path}")
|
|
275
|
+
|
|
276
|
+
super().__init__(
|
|
277
|
+
command="fastmcp",
|
|
278
|
+
args=["run", str(script_path)],
|
|
279
|
+
env=env,
|
|
280
|
+
cwd=cwd,
|
|
281
|
+
keep_alive=keep_alive,
|
|
282
|
+
log_file=log_file,
|
|
283
|
+
)
|
|
284
|
+
self.script_path = script_path
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class NodeStdioTransport(StdioTransport):
|
|
288
|
+
"""Transport for running Node.js scripts."""
|
|
289
|
+
|
|
290
|
+
def __init__(
|
|
291
|
+
self,
|
|
292
|
+
script_path: str | Path,
|
|
293
|
+
args: list[str] | None = None,
|
|
294
|
+
env: dict[str, str] | None = None,
|
|
295
|
+
cwd: str | None = None,
|
|
296
|
+
node_cmd: str = "node",
|
|
297
|
+
keep_alive: bool | None = None,
|
|
298
|
+
log_file: Path | TextIO | None = None,
|
|
299
|
+
):
|
|
300
|
+
"""
|
|
301
|
+
Initialize a Node transport.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
script_path: Path to the Node.js script to run
|
|
305
|
+
args: Additional arguments to pass to the script
|
|
306
|
+
env: Environment variables to set for the subprocess
|
|
307
|
+
cwd: Current working directory for the subprocess
|
|
308
|
+
node_cmd: Node.js command to use (default: "node")
|
|
309
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
310
|
+
Defaults to True. When True, the subprocess remains active
|
|
311
|
+
after the connection context exits, allowing reuse in
|
|
312
|
+
subsequent connections.
|
|
313
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
314
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
315
|
+
if not provided. When a Path is provided, the file will be created
|
|
316
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
317
|
+
errors will be written to this file instead of appearing in the console.
|
|
318
|
+
"""
|
|
319
|
+
script_path = Path(script_path).resolve()
|
|
320
|
+
if not script_path.is_file():
|
|
321
|
+
raise FileNotFoundError(f"Script not found: {script_path}")
|
|
322
|
+
if not str(script_path).endswith(".js"):
|
|
323
|
+
raise ValueError(f"Not a JavaScript script: {script_path}")
|
|
324
|
+
|
|
325
|
+
full_args = [str(script_path)]
|
|
326
|
+
if args:
|
|
327
|
+
full_args.extend(args)
|
|
328
|
+
|
|
329
|
+
super().__init__(
|
|
330
|
+
command=node_cmd,
|
|
331
|
+
args=full_args,
|
|
332
|
+
env=env,
|
|
333
|
+
cwd=cwd,
|
|
334
|
+
keep_alive=keep_alive,
|
|
335
|
+
log_file=log_file,
|
|
336
|
+
)
|
|
337
|
+
self.script_path = script_path
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class UvStdioTransport(StdioTransport):
|
|
341
|
+
"""Transport for running commands via the uv tool."""
|
|
342
|
+
|
|
343
|
+
def __init__(
|
|
344
|
+
self,
|
|
345
|
+
command: str,
|
|
346
|
+
args: list[str] | None = None,
|
|
347
|
+
module: bool = False,
|
|
348
|
+
project_directory: Path | None = None,
|
|
349
|
+
python_version: str | None = None,
|
|
350
|
+
with_packages: list[str] | None = None,
|
|
351
|
+
with_requirements: Path | None = None,
|
|
352
|
+
env_vars: dict[str, str] | None = None,
|
|
353
|
+
keep_alive: bool | None = None,
|
|
354
|
+
):
|
|
355
|
+
# Basic validation
|
|
356
|
+
if project_directory and not project_directory.exists():
|
|
357
|
+
raise NotADirectoryError(
|
|
358
|
+
f"Project directory not found: {project_directory}"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Create Environment from provided parameters (internal use)
|
|
362
|
+
env_config = UVEnvironment(
|
|
363
|
+
python=python_version,
|
|
364
|
+
dependencies=with_packages,
|
|
365
|
+
requirements=with_requirements,
|
|
366
|
+
project=project_directory,
|
|
367
|
+
editable=None, # Not exposed in this transport
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Build uv arguments using the config
|
|
371
|
+
uv_args: list[str] = []
|
|
372
|
+
|
|
373
|
+
# Check if we need any environment setup
|
|
374
|
+
if env_config._must_run_with_uv():
|
|
375
|
+
# Use the config to build args, but we need to handle the command differently
|
|
376
|
+
# since transport has specific needs
|
|
377
|
+
uv_args = ["run"]
|
|
378
|
+
|
|
379
|
+
if python_version:
|
|
380
|
+
uv_args.extend(["--python", python_version])
|
|
381
|
+
if project_directory:
|
|
382
|
+
uv_args.extend(["--directory", str(project_directory)])
|
|
383
|
+
|
|
384
|
+
# Note: Don't add fastmcp as dependency here, transport is for general use
|
|
385
|
+
for pkg in with_packages or []:
|
|
386
|
+
uv_args.extend(["--with", pkg])
|
|
387
|
+
if with_requirements:
|
|
388
|
+
uv_args.extend(["--with-requirements", str(with_requirements)])
|
|
389
|
+
else:
|
|
390
|
+
# No environment setup needed
|
|
391
|
+
uv_args = ["run"]
|
|
392
|
+
|
|
393
|
+
if module:
|
|
394
|
+
uv_args.append("--module")
|
|
395
|
+
|
|
396
|
+
if not args:
|
|
397
|
+
args = []
|
|
398
|
+
|
|
399
|
+
uv_args.extend([command, *args])
|
|
400
|
+
|
|
401
|
+
# Get environment with any additional variables
|
|
402
|
+
env: dict[str, str] | None = None
|
|
403
|
+
if env_vars or project_directory:
|
|
404
|
+
env = os.environ.copy()
|
|
405
|
+
if project_directory:
|
|
406
|
+
env["UV_PROJECT_DIR"] = str(project_directory)
|
|
407
|
+
if env_vars:
|
|
408
|
+
env.update(env_vars)
|
|
409
|
+
|
|
410
|
+
super().__init__(
|
|
411
|
+
command="uv",
|
|
412
|
+
args=uv_args,
|
|
413
|
+
env=env,
|
|
414
|
+
cwd=None, # Use --directory flag instead of cwd
|
|
415
|
+
keep_alive=keep_alive,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class UvxStdioTransport(StdioTransport):
|
|
420
|
+
"""Transport for running commands via the uvx tool."""
|
|
421
|
+
|
|
422
|
+
def __init__(
|
|
423
|
+
self,
|
|
424
|
+
tool_name: str,
|
|
425
|
+
tool_args: list[str] | None = None,
|
|
426
|
+
project_directory: str | None = None,
|
|
427
|
+
python_version: str | None = None,
|
|
428
|
+
with_packages: list[str] | None = None,
|
|
429
|
+
from_package: str | None = None,
|
|
430
|
+
env_vars: dict[str, str] | None = None,
|
|
431
|
+
keep_alive: bool | None = None,
|
|
432
|
+
):
|
|
433
|
+
"""
|
|
434
|
+
Initialize a Uvx transport.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
tool_name: Name of the tool to run via uvx
|
|
438
|
+
tool_args: Arguments to pass to the tool
|
|
439
|
+
project_directory: Project directory (for package resolution)
|
|
440
|
+
python_version: Python version to use
|
|
441
|
+
with_packages: Additional packages to include
|
|
442
|
+
from_package: Package to install the tool from
|
|
443
|
+
env_vars: Additional environment variables
|
|
444
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
445
|
+
Defaults to True. When True, the subprocess remains active
|
|
446
|
+
after the connection context exits, allowing reuse in
|
|
447
|
+
subsequent connections.
|
|
448
|
+
"""
|
|
449
|
+
# Basic validation
|
|
450
|
+
if project_directory and not Path(project_directory).exists():
|
|
451
|
+
raise NotADirectoryError(
|
|
452
|
+
f"Project directory not found: {project_directory}"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Build uvx arguments
|
|
456
|
+
uvx_args: list[str] = []
|
|
457
|
+
if python_version:
|
|
458
|
+
uvx_args.extend(["--python", python_version])
|
|
459
|
+
if from_package:
|
|
460
|
+
uvx_args.extend(["--from", from_package])
|
|
461
|
+
for pkg in with_packages or []:
|
|
462
|
+
uvx_args.extend(["--with", pkg])
|
|
463
|
+
|
|
464
|
+
# Add the tool name and tool args
|
|
465
|
+
uvx_args.append(tool_name)
|
|
466
|
+
if tool_args:
|
|
467
|
+
uvx_args.extend(tool_args)
|
|
468
|
+
|
|
469
|
+
env: dict[str, str] | None = None
|
|
470
|
+
if env_vars:
|
|
471
|
+
env = os.environ.copy()
|
|
472
|
+
env.update(env_vars)
|
|
473
|
+
|
|
474
|
+
super().__init__(
|
|
475
|
+
command="uvx",
|
|
476
|
+
args=uvx_args,
|
|
477
|
+
env=env,
|
|
478
|
+
cwd=project_directory,
|
|
479
|
+
keep_alive=keep_alive,
|
|
480
|
+
)
|
|
481
|
+
self.tool_name: str = tool_name
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class NpxStdioTransport(StdioTransport):
|
|
485
|
+
"""Transport for running commands via the npx tool."""
|
|
486
|
+
|
|
487
|
+
def __init__(
|
|
488
|
+
self,
|
|
489
|
+
package: str,
|
|
490
|
+
args: list[str] | None = None,
|
|
491
|
+
project_directory: str | None = None,
|
|
492
|
+
env_vars: dict[str, str] | None = None,
|
|
493
|
+
use_package_lock: bool = True,
|
|
494
|
+
keep_alive: bool | None = None,
|
|
495
|
+
):
|
|
496
|
+
"""
|
|
497
|
+
Initialize an Npx transport.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
package: Name of the npm package to run
|
|
501
|
+
args: Arguments to pass to the package command
|
|
502
|
+
project_directory: Project directory with package.json
|
|
503
|
+
env_vars: Additional environment variables
|
|
504
|
+
use_package_lock: Whether to use package-lock.json (--prefer-offline)
|
|
505
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
506
|
+
Defaults to True. When True, the subprocess remains active
|
|
507
|
+
after the connection context exits, allowing reuse in
|
|
508
|
+
subsequent connections.
|
|
509
|
+
"""
|
|
510
|
+
# verify npx is installed
|
|
511
|
+
if shutil.which("npx") is None:
|
|
512
|
+
raise ValueError("Command 'npx' not found")
|
|
513
|
+
|
|
514
|
+
# Basic validation
|
|
515
|
+
if project_directory and not Path(project_directory).exists():
|
|
516
|
+
raise NotADirectoryError(
|
|
517
|
+
f"Project directory not found: {project_directory}"
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
# Build npx arguments
|
|
521
|
+
npx_args = []
|
|
522
|
+
if use_package_lock:
|
|
523
|
+
npx_args.append("--prefer-offline")
|
|
524
|
+
|
|
525
|
+
# Add the package name and args
|
|
526
|
+
npx_args.append(package)
|
|
527
|
+
if args:
|
|
528
|
+
npx_args.extend(args)
|
|
529
|
+
|
|
530
|
+
# Get environment with any additional variables
|
|
531
|
+
env = None
|
|
532
|
+
if env_vars:
|
|
533
|
+
env = os.environ.copy()
|
|
534
|
+
env.update(env_vars)
|
|
535
|
+
|
|
536
|
+
super().__init__(
|
|
537
|
+
command="npx",
|
|
538
|
+
args=npx_args,
|
|
539
|
+
env=env,
|
|
540
|
+
cwd=project_directory,
|
|
541
|
+
keep_alive=keep_alive,
|
|
542
|
+
)
|
|
543
|
+
self.package = package
|
|
@@ -123,7 +123,7 @@ set_up_component_manager(server=mcp, required_scopes=["mcp:write"])
|
|
|
123
123
|
mounted = FastMCP(name="Component Manager", instructions="This is a test server with component manager.", auth=auth)
|
|
124
124
|
set_up_component_manager(server=mounted, required_scopes=["mounted:write"])
|
|
125
125
|
|
|
126
|
-
mcp.mount(server=mounted,
|
|
126
|
+
mcp.mount(server=mounted, namespace="mo")
|
|
127
127
|
```
|
|
128
128
|
|
|
129
129
|
This allows you to grant different levels of access:
|
|
@@ -146,15 +146,9 @@ curl -X POST \
|
|
|
146
146
|
|
|
147
147
|
## ⚙️ How It Works
|
|
148
148
|
|
|
149
|
-
- `set_up_component_manager()` registers
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## 🧩 Extending
|
|
156
|
-
|
|
157
|
-
You can subclass `ComponentService` for custom behavior or mount its routes elsewhere as needed.
|
|
149
|
+
- `set_up_component_manager()` registers HTTP routes for tools, resources, and prompts.
|
|
150
|
+
- Each endpoint calls `server.enable()` or `server.disable()` with the component name.
|
|
151
|
+
- Returns a success message in JSON.
|
|
158
152
|
|
|
159
153
|
---
|
|
160
154
|
|