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,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, prefix="mo")
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 API routes for tools, resources, and prompts.
150
- - The `ComponentService` class exposes async methods to enable/disable components.
151
- - Each endpoint returns a success message in JSON or a 404 error if the component isn't found.
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
 
@@ -1,4 +1,3 @@
1
1
  from .component_manager import set_up_component_manager
2
- from .component_service import ComponentService
3
2
 
4
- __all__ = ["ComponentService", "set_up_component_manager"]
3
+ __all__ = ["set_up_component_manager"]