fastmcp 2.12.4__py3-none-any.whl → 2.13.0rc1__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 (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +2 -2
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/logging.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from collections.abc import Awaitable, Callable
2
+ from logging import Logger
2
3
  from typing import TypeAlias
3
4
 
4
5
  from mcp.client.session import LoggingFnT
@@ -6,7 +7,8 @@ from mcp.types import LoggingMessageNotificationParams
6
7
 
7
8
  from fastmcp.utilities.logging import get_logger
8
9
 
9
- logger = get_logger(__name__)
10
+ logger: Logger = get_logger(name=__name__)
11
+ from_server_logger: Logger = get_logger(name="fastmcp.client.from_server")
10
12
 
11
13
  LogMessage: TypeAlias = LoggingMessageNotificationParams
12
14
  LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
@@ -14,30 +16,32 @@ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
14
16
 
15
17
  async def default_log_handler(message: LogMessage) -> None:
16
18
  """Default handler that properly routes server log messages to appropriate log levels."""
17
- msg = message.data.get("msg", str(message))
18
- extra = message.data.get("extra", {})
19
+ # data can be any JSON-serializable type, not just a dict
20
+ data = message.data
19
21
 
20
22
  # Map MCP log levels to Python logging levels
21
23
  level_map = {
22
- "debug": logger.debug,
23
- "info": logger.info,
24
- "notice": logger.info, # Python doesn't have 'notice', map to info
25
- "warning": logger.warning,
26
- "error": logger.error,
27
- "critical": logger.critical,
28
- "alert": logger.critical, # Map alert to critical
29
- "emergency": logger.critical, # Map emergency to critical
24
+ "debug": from_server_logger.debug,
25
+ "info": from_server_logger.info,
26
+ "notice": from_server_logger.info, # Python doesn't have 'notice', map to info
27
+ "warning": from_server_logger.warning,
28
+ "error": from_server_logger.error,
29
+ "critical": from_server_logger.critical,
30
+ "alert": from_server_logger.critical, # Map alert to critical
31
+ "emergency": from_server_logger.critical, # Map emergency to critical
30
32
  }
31
33
 
32
34
  # Get the appropriate logging function based on the message level
33
35
  log_fn = level_map.get(message.level.lower(), logger.info)
34
36
 
35
37
  # Include logger name if available
38
+ msg_prefix: str = f"Received {message.level.upper()} from server"
39
+
36
40
  if message.logger:
37
- msg = f"[{message.logger}] {msg}"
41
+ msg_prefix += f" ({message.logger})"
38
42
 
39
- # Log with appropriate level and extra data
40
- log_fn(f"Server log: {msg}", extra=extra)
43
+ # Log with appropriate level and data
44
+ log_fn(msg=f"{msg_prefix}: {data}")
41
45
 
42
46
 
43
47
  def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
@@ -7,17 +7,26 @@ and display styled responses to users.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import asyncio
11
10
  from dataclasses import dataclass
12
11
 
12
+ import anyio
13
13
  from starlette.applications import Starlette
14
14
  from starlette.requests import Request
15
- from starlette.responses import HTMLResponse
16
15
  from starlette.routing import Route
17
16
  from uvicorn import Config, Server
18
17
 
19
18
  from fastmcp.utilities.http import find_available_port
20
19
  from fastmcp.utilities.logging import get_logger
20
+ from fastmcp.utilities.ui import (
21
+ HELPER_TEXT_STYLES,
22
+ INFO_BOX_STYLES,
23
+ STATUS_MESSAGE_STYLES,
24
+ create_info_box,
25
+ create_logo,
26
+ create_page,
27
+ create_secure_html_response,
28
+ create_status_message,
29
+ )
21
30
 
22
31
  logger = get_logger(__name__)
23
32
 
@@ -29,155 +38,39 @@ def create_callback_html(
29
38
  server_url: str | None = None,
30
39
  ) -> str:
31
40
  """Create a styled HTML response for OAuth callbacks."""
32
- logo_url = "https://gofastmcp.com/assets/brand/blue-logo.png"
33
-
34
41
  # Build the main status message
35
- if is_success:
36
- status_title = "Authentication successful"
37
- status_icon = "✓"
38
- icon_bg = "#10b98120"
39
- else:
40
- status_title = "Authentication failed"
41
- status_icon = "✕"
42
- icon_bg = "#ef444420"
42
+ status_title = (
43
+ "Authentication successful" if is_success else "Authentication failed"
44
+ )
43
45
 
44
46
  # Add detail info box for both success and error cases
45
47
  detail_info = ""
46
48
  if is_success and server_url:
47
- detail_info = f"""
48
- <div class="info-box">
49
- Connected to: <strong>{server_url}</strong>
50
- </div>
51
- """
49
+ detail_info = create_info_box(f"Connected to: {server_url}", centered=True)
52
50
  elif not is_success:
53
- detail_info = f"""
54
- <div class="info-box error">
55
- {message}
56
- </div>
57
- """
58
-
59
- return f"""
60
- <!DOCTYPE html>
61
- <html lang="en">
62
- <head>
63
- <meta charset="UTF-8">
64
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
65
- <title>{title}</title>
66
- <style>
67
- * {{
68
- margin: 0;
69
- padding: 0;
70
- box-sizing: border-box;
71
- }}
72
-
73
- body {{
74
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
75
- margin: 0;
76
- padding: 0;
77
- min-height: 100vh;
78
- display: flex;
79
- align-items: center;
80
- justify-content: center;
81
- background: #ffffff;
82
- color: #0a0a0a;
83
- }}
84
-
85
- .container {{
86
- background: #ffffff;
87
- border: 1px solid #e5e5e5;
88
- padding: 3rem 2rem;
89
- border-radius: 0.75rem;
90
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
91
- text-align: center;
92
- max-width: 28rem;
93
- margin: 1rem;
94
- position: relative;
95
- }}
96
-
97
- .logo {{
98
- width: 60px;
99
- height: auto;
100
- margin-bottom: 2rem;
101
- display: block;
102
- margin-left: auto;
103
- margin-right: auto;
104
- }}
105
-
106
- .status-message {{
107
- display: flex;
108
- align-items: center;
109
- justify-content: center;
110
- gap: 0.75rem;
111
- margin-bottom: 1.5rem;
112
- }}
113
-
114
- .status-icon {{
115
- font-size: 1.5rem;
116
- line-height: 1;
117
- display: inline-flex;
118
- align-items: center;
119
- justify-content: center;
120
- width: 2rem;
121
- height: 2rem;
122
- background: {icon_bg};
123
- border-radius: 0.5rem;
124
- flex-shrink: 0;
125
- }}
126
-
127
- .message {{
128
- font-size: 1.125rem;
129
- line-height: 1.75;
130
- color: #0a0a0a;
131
- font-weight: 600;
132
- text-align: left;
133
- }}
134
-
135
- .info-box {{
136
- background: #f5f5f5;
137
- border: 1px solid #e5e5e5;
138
- border-radius: 0.5rem;
139
- padding: 0.875rem;
140
- margin: 1.25rem 0;
141
- font-size: 0.875rem;
142
- color: #525252;
143
- font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
144
- text-align: left;
145
- }}
146
-
147
- .info-box.error {{
148
- background: #fef2f2;
149
- border-color: #fecaca;
150
- color: #991b1b;
151
- }}
152
-
153
- .info-box strong {{
154
- color: #0a0a0a;
155
- font-weight: 600;
156
- }}
157
-
158
- .close-instruction {{
159
- font-size: 0.875rem;
160
- color: #737373;
161
- margin-top: 1.5rem;
162
- }}
163
- </style>
164
- </head>
165
- <body>
51
+ detail_info = create_info_box(message, is_error=True, centered=True)
52
+
53
+ # Build the page content
54
+ content = f"""
166
55
  <div class="container">
167
- <img src="{logo_url}" alt="FastMCP" class="logo" />
168
- <div class="status-message">
169
- <span class="status-icon">{status_icon}</span>
170
- <div class="message">{status_title}</div>
171
- </div>
56
+ {create_logo()}
57
+ {create_status_message(status_title, is_success=is_success)}
172
58
  {detail_info}
173
59
  <div class="close-instruction">
174
60
  You can safely close this tab now.
175
61
  </div>
176
62
  </div>
177
- </body>
178
- </html>
179
63
  """
180
64
 
65
+ # Additional styles needed for this page
66
+ additional_styles = STATUS_MESSAGE_STYLES + INFO_BOX_STYLES + HELPER_TEXT_STYLES
67
+
68
+ return create_page(
69
+ content=content,
70
+ title=title,
71
+ additional_styles=additional_styles,
72
+ )
73
+
181
74
 
182
75
  @dataclass
183
76
  class CallbackResponse:
@@ -194,11 +87,21 @@ class CallbackResponse:
194
87
  return {k: v for k, v in self.__dict__.items() if v is not None}
195
88
 
196
89
 
90
+ @dataclass
91
+ class OAuthCallbackResult:
92
+ """Container for OAuth callback results, used with anyio.Event for async coordination."""
93
+
94
+ code: str | None = None
95
+ state: str | None = None
96
+ error: Exception | None = None
97
+
98
+
197
99
  def create_oauth_callback_server(
198
100
  port: int,
199
101
  callback_path: str = "/callback",
200
102
  server_url: str | None = None,
201
- response_future: asyncio.Future | None = None,
103
+ result_container: OAuthCallbackResult | None = None,
104
+ result_ready: anyio.Event | None = None,
202
105
  ) -> Server:
203
106
  """
204
107
  Create an OAuth callback server.
@@ -207,7 +110,8 @@ def create_oauth_callback_server(
207
110
  port: The port to run the server on
208
111
  callback_path: The path to listen for OAuth redirects on
209
112
  server_url: Optional server URL to display in success messages
210
- response_future: Optional future to resolve when OAuth callback is received
113
+ result_container: Optional container to store callback results
114
+ result_ready: Optional event to signal when callback is received
211
115
 
212
116
  Returns:
213
117
  Configured uvicorn Server instance (not yet running)
@@ -221,32 +125,36 @@ def create_oauth_callback_server(
221
125
  if callback_response.error:
222
126
  error_desc = callback_response.error_description or "Unknown error"
223
127
 
224
- # Resolve future with exception if provided
225
- if response_future and not response_future.done():
226
- response_future.set_exception(
227
- RuntimeError(
228
- f"OAuth error: {callback_response.error} - {error_desc}"
229
- )
230
- )
128
+ # Create user-friendly error messages
129
+ if callback_response.error == "access_denied":
130
+ user_message = "Access was denied by the authorization server."
131
+ else:
132
+ user_message = f"Authorization failed: {error_desc}"
133
+
134
+ # Store error and signal completion if result tracking provided
135
+ if result_container is not None and result_ready is not None:
136
+ result_container.error = RuntimeError(user_message)
137
+ result_ready.set()
231
138
 
232
- return HTMLResponse(
139
+ return create_secure_html_response(
233
140
  create_callback_html(
234
- f"FastMCP OAuth Error: {callback_response.error}<br>{error_desc}",
141
+ user_message,
235
142
  is_success=False,
236
143
  ),
237
144
  status_code=400,
238
145
  )
239
146
 
240
147
  if not callback_response.code:
241
- # Resolve future with exception if provided
242
- if response_future and not response_future.done():
243
- response_future.set_exception(
244
- RuntimeError("OAuth callback missing authorization code")
245
- )
148
+ user_message = "No authorization code was received from the server."
246
149
 
247
- return HTMLResponse(
150
+ # Store error and signal completion if result tracking provided
151
+ if result_container is not None and result_ready is not None:
152
+ result_container.error = RuntimeError(user_message)
153
+ result_ready.set()
154
+
155
+ return create_secure_html_response(
248
156
  create_callback_html(
249
- "FastMCP OAuth Error: No authorization code received",
157
+ user_message,
250
158
  is_success=False,
251
159
  ),
252
160
  status_code=400,
@@ -254,29 +162,30 @@ def create_oauth_callback_server(
254
162
 
255
163
  # Check for missing state parameter (indicates OAuth flow issue)
256
164
  if callback_response.state is None:
257
- # Resolve future with exception if provided
258
- if response_future and not response_future.done():
259
- response_future.set_exception(
260
- RuntimeError(
261
- "OAuth server did not return state parameter - authentication failed"
262
- )
263
- )
264
-
265
- return HTMLResponse(
165
+ user_message = (
166
+ "The OAuth server did not return the expected state parameter."
167
+ )
168
+
169
+ # Store error and signal completion if result tracking provided
170
+ if result_container is not None and result_ready is not None:
171
+ result_container.error = RuntimeError(user_message)
172
+ result_ready.set()
173
+
174
+ return create_secure_html_response(
266
175
  create_callback_html(
267
- "FastMCP OAuth Error: Authentication failed<br>The OAuth server did not return the expected state parameter",
176
+ user_message,
268
177
  is_success=False,
269
178
  ),
270
179
  status_code=400,
271
180
  )
272
181
 
273
- # Success case
274
- if response_future and not response_future.done():
275
- response_future.set_result(
276
- (callback_response.code, callback_response.state)
277
- )
182
+ # Success case - store result and signal completion if result tracking provided
183
+ if result_container is not None and result_ready is not None:
184
+ result_container.code = callback_response.code
185
+ result_container.state = callback_response.state
186
+ result_ready.set()
278
187
 
279
- return HTMLResponse(
188
+ return create_secure_html_response(
280
189
  create_callback_html("", is_success=True, server_url=server_url)
281
190
  )
282
191
 
@@ -289,6 +198,7 @@ def create_oauth_callback_server(
289
198
  port=port,
290
199
  lifespan="off",
291
200
  log_level="warning",
201
+ ws="websockets-sansio",
292
202
  )
293
203
  )
294
204
 
@@ -8,7 +8,7 @@ import sys
8
8
  import warnings
9
9
  from collections.abc import AsyncIterator
10
10
  from pathlib import Path
11
- from typing import Any, Literal, TypeVar, cast, overload
11
+ from typing import Any, Literal, TextIO, TypeVar, cast, overload
12
12
 
13
13
  import anyio
14
14
  import httpx
@@ -313,6 +313,7 @@ class StdioTransport(ClientTransport):
313
313
  env: dict[str, str] | None = None,
314
314
  cwd: str | None = None,
315
315
  keep_alive: bool | None = None,
316
+ log_file: Path | TextIO | None = None,
316
317
  ):
317
318
  """
318
319
  Initialize a Stdio transport.
@@ -326,6 +327,11 @@ class StdioTransport(ClientTransport):
326
327
  Defaults to True. When True, the subprocess remains active
327
328
  after the connection context exits, allowing reuse in
328
329
  subsequent connections.
330
+ log_file: Optional path or file-like object where subprocess stderr will
331
+ be written. Can be a Path or TextIO object. Defaults to sys.stderr
332
+ if not provided. When a Path is provided, the file will be created
333
+ if it doesn't exist, or appended to if it does. When set, server
334
+ errors will be written to this file instead of appearing in the console.
329
335
  """
330
336
  self.command = command
331
337
  self.args = args
@@ -334,6 +340,7 @@ class StdioTransport(ClientTransport):
334
340
  if keep_alive is None:
335
341
  keep_alive = True
336
342
  self.keep_alive = keep_alive
343
+ self.log_file = log_file
337
344
 
338
345
  self._session: ClientSession | None = None
339
346
  self._connect_task: asyncio.Task | None = None
@@ -368,6 +375,7 @@ class StdioTransport(ClientTransport):
368
375
  args=self.args,
369
376
  env=self.env,
370
377
  cwd=self.cwd,
378
+ log_file=self.log_file,
371
379
  session_kwargs=session_kwargs,
372
380
  ready_event=self._ready_event,
373
381
  stop_event=self._stop_event,
@@ -421,6 +429,7 @@ async def _stdio_transport_connect_task(
421
429
  args: list[str],
422
430
  env: dict[str, str] | None,
423
431
  cwd: str | None,
432
+ log_file: Path | TextIO | None,
424
433
  session_kwargs: SessionKwargs,
425
434
  ready_event: anyio.Event,
426
435
  stop_event: anyio.Event,
@@ -438,7 +447,19 @@ async def _stdio_transport_connect_task(
438
447
  env=env,
439
448
  cwd=cwd,
440
449
  )
441
- transport = await stack.enter_async_context(stdio_client(server_params))
450
+ # Handle log_file: Path needs to be opened, TextIO used as-is
451
+ if log_file is None:
452
+ log_file_handle = sys.stderr
453
+ elif isinstance(log_file, Path):
454
+ log_file_handle = open(log_file, "a")
455
+ stack.callback(log_file_handle.close)
456
+ else:
457
+ # Must be TextIO - use it directly
458
+ log_file_handle = log_file
459
+
460
+ transport = await stack.enter_async_context(
461
+ stdio_client(server_params, errlog=log_file_handle)
462
+ )
442
463
  read_stream, write_stream = transport
443
464
  session_future.set_result(
444
465
  await stack.enter_async_context(
@@ -471,6 +492,7 @@ class PythonStdioTransport(StdioTransport):
471
492
  cwd: str | None = None,
472
493
  python_cmd: str = sys.executable,
473
494
  keep_alive: bool | None = None,
495
+ log_file: Path | TextIO | None = None,
474
496
  ):
475
497
  """
476
498
  Initialize a Python transport.
@@ -485,6 +507,11 @@ class PythonStdioTransport(StdioTransport):
485
507
  Defaults to True. When True, the subprocess remains active
486
508
  after the connection context exits, allowing reuse in
487
509
  subsequent connections.
510
+ log_file: Optional path or file-like object where subprocess stderr will
511
+ be written. Can be a Path or TextIO object. Defaults to sys.stderr
512
+ if not provided. When a Path is provided, the file will be created
513
+ if it doesn't exist, or appended to if it does. When set, server
514
+ errors will be written to this file instead of appearing in the console.
488
515
  """
489
516
  script_path = Path(script_path).resolve()
490
517
  if not script_path.is_file():
@@ -502,6 +529,7 @@ class PythonStdioTransport(StdioTransport):
502
529
  env=env,
503
530
  cwd=cwd,
504
531
  keep_alive=keep_alive,
532
+ log_file=log_file,
505
533
  )
506
534
  self.script_path = script_path
507
535
 
@@ -516,6 +544,7 @@ class FastMCPStdioTransport(StdioTransport):
516
544
  env: dict[str, str] | None = None,
517
545
  cwd: str | None = None,
518
546
  keep_alive: bool | None = None,
547
+ log_file: Path | TextIO | None = None,
519
548
  ):
520
549
  script_path = Path(script_path).resolve()
521
550
  if not script_path.is_file():
@@ -529,6 +558,7 @@ class FastMCPStdioTransport(StdioTransport):
529
558
  env=env,
530
559
  cwd=cwd,
531
560
  keep_alive=keep_alive,
561
+ log_file=log_file,
532
562
  )
533
563
  self.script_path = script_path
534
564
 
@@ -544,6 +574,7 @@ class NodeStdioTransport(StdioTransport):
544
574
  cwd: str | None = None,
545
575
  node_cmd: str = "node",
546
576
  keep_alive: bool | None = None,
577
+ log_file: Path | TextIO | None = None,
547
578
  ):
548
579
  """
549
580
  Initialize a Node transport.
@@ -558,6 +589,11 @@ class NodeStdioTransport(StdioTransport):
558
589
  Defaults to True. When True, the subprocess remains active
559
590
  after the connection context exits, allowing reuse in
560
591
  subsequent connections.
592
+ log_file: Optional path or file-like object where subprocess stderr will
593
+ be written. Can be a Path or TextIO object. Defaults to sys.stderr
594
+ if not provided. When a Path is provided, the file will be created
595
+ if it doesn't exist, or appended to if it does. When set, server
596
+ errors will be written to this file instead of appearing in the console.
561
597
  """
562
598
  script_path = Path(script_path).resolve()
563
599
  if not script_path.is_file():
@@ -570,7 +606,12 @@ class NodeStdioTransport(StdioTransport):
570
606
  full_args.extend(args)
571
607
 
572
608
  super().__init__(
573
- command=node_cmd, args=full_args, env=env, cwd=cwd, keep_alive=keep_alive
609
+ command=node_cmd,
610
+ args=full_args,
611
+ env=env,
612
+ cwd=cwd,
613
+ keep_alive=keep_alive,
614
+ log_file=log_file,
574
615
  )
575
616
  self.script_path = script_path
576
617
 
@@ -583,15 +624,15 @@ class UvStdioTransport(StdioTransport):
583
624
  command: str,
584
625
  args: list[str] | None = None,
585
626
  module: bool = False,
586
- project_directory: str | None = None,
627
+ project_directory: Path | None = None,
587
628
  python_version: str | None = None,
588
629
  with_packages: list[str] | None = None,
589
- with_requirements: str | None = None,
630
+ with_requirements: Path | None = None,
590
631
  env_vars: dict[str, str] | None = None,
591
632
  keep_alive: bool | None = None,
592
633
  ):
593
634
  # Basic validation
594
- if project_directory and not Path(project_directory).exists():
635
+ if project_directory and not project_directory.exists():
595
636
  raise NotADirectoryError(
596
637
  f"Project directory not found: {project_directory}"
597
638
  )
@@ -811,29 +852,42 @@ class FastMCPTransport(ClientTransport):
811
852
 
812
853
  # Create a cancel scope for the server task
813
854
  async with anyio.create_task_group() as tg:
814
- tg.start_soon(
815
- lambda: self.server._mcp_server.run(
816
- server_read,
817
- server_write,
818
- self.server._mcp_server.create_initialization_options(),
819
- raise_exceptions=self.raise_exceptions,
855
+ async with _enter_server_lifespan(server=self.server):
856
+ tg.start_soon(
857
+ lambda: self.server._mcp_server.run(
858
+ server_read,
859
+ server_write,
860
+ self.server._mcp_server.create_initialization_options(),
861
+ raise_exceptions=self.raise_exceptions,
862
+ )
820
863
  )
821
- )
822
864
 
823
- try:
824
- async with ClientSession(
825
- read_stream=client_read,
826
- write_stream=client_write,
827
- **session_kwargs,
828
- ) as client_session:
829
- yield client_session
830
- finally:
831
- tg.cancel_scope.cancel()
865
+ try:
866
+ async with ClientSession(
867
+ read_stream=client_read,
868
+ write_stream=client_write,
869
+ **session_kwargs,
870
+ ) as client_session:
871
+ yield client_session
872
+ finally:
873
+ tg.cancel_scope.cancel()
832
874
 
833
875
  def __repr__(self) -> str:
834
876
  return f"<FastMCPTransport(server='{self.server.name}')>"
835
877
 
836
878
 
879
+ @contextlib.asynccontextmanager
880
+ async def _enter_server_lifespan(
881
+ server: FastMCP | FastMCP1Server,
882
+ ) -> AsyncIterator[None]:
883
+ """Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers."""
884
+ if isinstance(server, FastMCP):
885
+ async with server._lifespan_manager():
886
+ yield
887
+ else:
888
+ yield
889
+
890
+
837
891
  class MCPConfigTransport(ClientTransport):
838
892
  """Transport for connecting to one or more MCP servers defined in an MCPConfig.
839
893
 
@@ -41,7 +41,7 @@ class ComponentService:
41
41
  return tool
42
42
 
43
43
  # 2. Check mounted servers using the filtered protocol path.
44
- for mounted in reversed(self._tool_manager._mounted_servers):
44
+ for mounted in reversed(self._server._mounted_servers):
45
45
  if mounted.prefix:
46
46
  if key.startswith(f"{mounted.prefix}_"):
47
47
  tool_key = key.removeprefix(f"{mounted.prefix}_")
@@ -70,7 +70,7 @@ class ComponentService:
70
70
  return tool
71
71
 
72
72
  # 2. Check mounted servers using the filtered protocol path.
73
- for mounted in reversed(self._tool_manager._mounted_servers):
73
+ for mounted in reversed(self._server._mounted_servers):
74
74
  if mounted.prefix:
75
75
  if key.startswith(f"{mounted.prefix}_"):
76
76
  tool_key = key.removeprefix(f"{mounted.prefix}_")
@@ -103,7 +103,7 @@ class ComponentService:
103
103
  return template
104
104
 
105
105
  # 2. Check mounted servers using the filtered protocol path.
106
- for mounted in reversed(self._resource_manager._mounted_servers):
106
+ for mounted in reversed(self._server._mounted_servers):
107
107
  if mounted.prefix:
108
108
  if has_resource_prefix(
109
109
  key,
@@ -146,7 +146,7 @@ class ComponentService:
146
146
  return template
147
147
 
148
148
  # 2. Check mounted servers using the filtered protocol path.
149
- for mounted in reversed(self._resource_manager._mounted_servers):
149
+ for mounted in reversed(self._server._mounted_servers):
150
150
  if mounted.prefix:
151
151
  if has_resource_prefix(
152
152
  key,
@@ -185,7 +185,7 @@ class ComponentService:
185
185
  return prompt
186
186
 
187
187
  # 2. Check mounted servers using the filtered protocol path.
188
- for mounted in reversed(self._prompt_manager._mounted_servers):
188
+ for mounted in reversed(self._server._mounted_servers):
189
189
  if mounted.prefix:
190
190
  if key.startswith(f"{mounted.prefix}_"):
191
191
  prompt_key = key.removeprefix(f"{mounted.prefix}_")
@@ -213,7 +213,7 @@ class ComponentService:
213
213
  return prompt
214
214
 
215
215
  # 2. Check mounted servers using the filtered protocol path.
216
- for mounted in reversed(self._prompt_manager._mounted_servers):
216
+ for mounted in reversed(self._server._mounted_servers):
217
217
  if mounted.prefix:
218
218
  if key.startswith(f"{mounted.prefix}_"):
219
219
  prompt_key = key.removeprefix(f"{mounted.prefix}_")