fastmcp 2.12.5__py3-none-any.whl → 2.14.0__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 (133) hide show
  1. fastmcp/__init__.py +2 -23
  2. fastmcp/cli/__init__.py +0 -3
  3. fastmcp/cli/__main__.py +5 -0
  4. fastmcp/cli/cli.py +19 -33
  5. fastmcp/cli/install/claude_code.py +6 -6
  6. fastmcp/cli/install/claude_desktop.py +3 -3
  7. fastmcp/cli/install/cursor.py +18 -12
  8. fastmcp/cli/install/gemini_cli.py +3 -3
  9. fastmcp/cli/install/mcp_json.py +3 -3
  10. fastmcp/cli/install/shared.py +0 -15
  11. fastmcp/cli/run.py +13 -8
  12. fastmcp/cli/tasks.py +110 -0
  13. fastmcp/client/__init__.py +9 -9
  14. fastmcp/client/auth/oauth.py +123 -225
  15. fastmcp/client/client.py +697 -95
  16. fastmcp/client/elicitation.py +11 -5
  17. fastmcp/client/logging.py +18 -14
  18. fastmcp/client/messages.py +7 -5
  19. fastmcp/client/oauth_callback.py +85 -171
  20. fastmcp/client/roots.py +2 -1
  21. fastmcp/client/sampling.py +1 -1
  22. fastmcp/client/tasks.py +614 -0
  23. fastmcp/client/transports.py +117 -30
  24. fastmcp/contrib/component_manager/__init__.py +1 -1
  25. fastmcp/contrib/component_manager/component_manager.py +2 -2
  26. fastmcp/contrib/component_manager/component_service.py +10 -26
  27. fastmcp/contrib/mcp_mixin/README.md +32 -1
  28. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  29. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  30. fastmcp/dependencies.py +25 -0
  31. fastmcp/experimental/sampling/handlers/openai.py +3 -3
  32. fastmcp/experimental/server/openapi/__init__.py +20 -21
  33. fastmcp/experimental/utilities/openapi/__init__.py +16 -47
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +54 -51
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +43 -21
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +161 -61
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -14
  45. fastmcp/server/auth/auth.py +197 -46
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1469 -298
  50. fastmcp/server/auth/oidc_proxy.py +91 -20
  51. fastmcp/server/auth/providers/auth0.py +40 -21
  52. fastmcp/server/auth/providers/aws.py +29 -3
  53. fastmcp/server/auth/providers/azure.py +312 -131
  54. fastmcp/server/auth/providers/debug.py +114 -0
  55. fastmcp/server/auth/providers/descope.py +86 -29
  56. fastmcp/server/auth/providers/discord.py +308 -0
  57. fastmcp/server/auth/providers/github.py +29 -8
  58. fastmcp/server/auth/providers/google.py +48 -9
  59. fastmcp/server/auth/providers/in_memory.py +29 -5
  60. fastmcp/server/auth/providers/introspection.py +281 -0
  61. fastmcp/server/auth/providers/jwt.py +48 -31
  62. fastmcp/server/auth/providers/oci.py +233 -0
  63. fastmcp/server/auth/providers/scalekit.py +238 -0
  64. fastmcp/server/auth/providers/supabase.py +188 -0
  65. fastmcp/server/auth/providers/workos.py +35 -17
  66. fastmcp/server/context.py +236 -116
  67. fastmcp/server/dependencies.py +503 -18
  68. fastmcp/server/elicitation.py +286 -48
  69. fastmcp/server/event_store.py +177 -0
  70. fastmcp/server/http.py +71 -20
  71. fastmcp/server/low_level.py +165 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +50 -39
  76. fastmcp/server/middleware/middleware.py +29 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi/__init__.py +35 -0
  80. fastmcp/{experimental/server → server}/openapi/components.py +15 -10
  81. fastmcp/{experimental/server → server}/openapi/routing.py +3 -3
  82. fastmcp/{experimental/server → server}/openapi/server.py +6 -5
  83. fastmcp/server/proxy.py +72 -48
  84. fastmcp/server/server.py +1415 -733
  85. fastmcp/server/tasks/__init__.py +21 -0
  86. fastmcp/server/tasks/capabilities.py +22 -0
  87. fastmcp/server/tasks/config.py +89 -0
  88. fastmcp/server/tasks/converters.py +205 -0
  89. fastmcp/server/tasks/handlers.py +356 -0
  90. fastmcp/server/tasks/keys.py +93 -0
  91. fastmcp/server/tasks/protocol.py +355 -0
  92. fastmcp/server/tasks/subscriptions.py +205 -0
  93. fastmcp/settings.py +125 -113
  94. fastmcp/tools/__init__.py +1 -1
  95. fastmcp/tools/tool.py +138 -55
  96. fastmcp/tools/tool_manager.py +30 -112
  97. fastmcp/tools/tool_transform.py +12 -21
  98. fastmcp/utilities/cli.py +67 -28
  99. fastmcp/utilities/components.py +10 -5
  100. fastmcp/utilities/inspect.py +79 -23
  101. fastmcp/utilities/json_schema.py +4 -4
  102. fastmcp/utilities/json_schema_type.py +8 -8
  103. fastmcp/utilities/logging.py +118 -8
  104. fastmcp/utilities/mcp_config.py +1 -2
  105. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  106. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  107. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  108. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +5 -5
  109. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  110. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  111. fastmcp/{experimental/utilities → utilities}/openapi/README.md +7 -35
  112. fastmcp/utilities/openapi/__init__.py +63 -0
  113. fastmcp/{experimental/utilities → utilities}/openapi/director.py +14 -15
  114. fastmcp/{experimental/utilities → utilities}/openapi/formatters.py +5 -5
  115. fastmcp/{experimental/utilities → utilities}/openapi/json_schema_converter.py +7 -3
  116. fastmcp/{experimental/utilities → utilities}/openapi/parser.py +37 -16
  117. fastmcp/utilities/tests.py +92 -5
  118. fastmcp/utilities/types.py +86 -16
  119. fastmcp/utilities/ui.py +626 -0
  120. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/METADATA +24 -15
  121. fastmcp-2.14.0.dist-info/RECORD +156 -0
  122. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/WHEEL +1 -1
  123. fastmcp/cli/claude.py +0 -135
  124. fastmcp/server/auth/providers/bearer.py +0 -25
  125. fastmcp/server/openapi.py +0 -1083
  126. fastmcp/utilities/openapi.py +0 -1568
  127. fastmcp/utilities/storage.py +0 -204
  128. fastmcp-2.12.5.dist-info/RECORD +0 -134
  129. fastmcp/{experimental/server → server}/openapi/README.md +0 -0
  130. fastmcp/{experimental/utilities → utilities}/openapi/models.py +3 -3
  131. fastmcp/{experimental/utilities → utilities}/openapi/schemas.py +2 -2
  132. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/entry_points.txt +0 -0
  133. {fastmcp-2.12.5.dist-info → fastmcp-2.14.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,9 +6,9 @@ import os
6
6
  import shutil
7
7
  import sys
8
8
  import warnings
9
- from collections.abc import AsyncIterator
9
+ from collections.abc import AsyncIterator, Callable
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
@@ -36,6 +36,7 @@ from fastmcp.client.auth.oauth import OAuth
36
36
  from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
37
37
  from fastmcp.server.dependencies import get_http_headers
38
38
  from fastmcp.server.server import FastMCP
39
+ from fastmcp.server.tasks.capabilities import get_task_capabilities
39
40
  from fastmcp.utilities.logging import get_logger
40
41
  from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
41
42
 
@@ -46,16 +47,16 @@ ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
46
47
 
47
48
  __all__ = [
48
49
  "ClientTransport",
49
- "SSETransport",
50
- "StreamableHttpTransport",
51
- "StdioTransport",
52
- "PythonStdioTransport",
53
50
  "FastMCPStdioTransport",
51
+ "FastMCPTransport",
54
52
  "NodeStdioTransport",
55
- "UvxStdioTransport",
56
- "UvStdioTransport",
57
53
  "NpxStdioTransport",
58
- "FastMCPTransport",
54
+ "PythonStdioTransport",
55
+ "SSETransport",
56
+ "StdioTransport",
57
+ "StreamableHttpTransport",
58
+ "UvStdioTransport",
59
+ "UvxStdioTransport",
59
60
  "infer_transport",
60
61
  ]
61
62
 
@@ -109,9 +110,8 @@ class ClientTransport(abc.ABC):
109
110
  # Basic representation for subclasses
110
111
  return f"<{self.__class__.__name__}>"
111
112
 
112
- async def close(self):
113
+ async def close(self): # noqa: B027
113
114
  """Close the transport."""
114
- pass
115
115
 
116
116
  def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
117
117
  if auth is not None:
@@ -141,10 +141,10 @@ class WSTransport(ClientTransport):
141
141
  ) -> AsyncIterator[ClientSession]:
142
142
  try:
143
143
  from mcp.client.websocket import websocket_client
144
- except ImportError:
144
+ except ImportError as e:
145
145
  raise ImportError(
146
146
  "The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
147
- )
147
+ ) from e
148
148
 
149
149
  async with websocket_client(self.url) as transport:
150
150
  read_stream, write_stream = transport
@@ -178,8 +178,8 @@ class SSETransport(ClientTransport):
178
178
 
179
179
  self.url = url
180
180
  self.headers = headers or {}
181
- self._set_auth(auth)
182
181
  self.httpx_client_factory = httpx_client_factory
182
+ self._set_auth(auth)
183
183
 
184
184
  if isinstance(sse_read_timeout, int | float):
185
185
  sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
@@ -187,7 +187,7 @@ class SSETransport(ClientTransport):
187
187
 
188
188
  def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
189
189
  if auth == "oauth":
190
- auth = OAuth(self.url)
190
+ auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
191
191
  elif isinstance(auth, str):
192
192
  auth = BearerAuth(auth)
193
193
  self.auth = auth
@@ -207,7 +207,7 @@ class SSETransport(ClientTransport):
207
207
  # instead we simply leave the kwarg out if it's not provided
208
208
  if self.sse_read_timeout is not None:
209
209
  client_kwargs["sse_read_timeout"] = self.sse_read_timeout.total_seconds()
210
- if session_kwargs.get("read_timeout_seconds", None) is not None:
210
+ if session_kwargs.get("read_timeout_seconds") is not None:
211
211
  read_timeout_seconds = cast(
212
212
  datetime.timedelta, session_kwargs.get("read_timeout_seconds")
213
213
  )
@@ -248,16 +248,18 @@ class StreamableHttpTransport(ClientTransport):
248
248
 
249
249
  self.url = url
250
250
  self.headers = headers or {}
251
- self._set_auth(auth)
252
251
  self.httpx_client_factory = httpx_client_factory
252
+ self._set_auth(auth)
253
253
 
254
254
  if isinstance(sse_read_timeout, int | float):
255
255
  sse_read_timeout = datetime.timedelta(seconds=float(sse_read_timeout))
256
256
  self.sse_read_timeout = sse_read_timeout
257
257
 
258
+ self._get_session_id_cb: Callable[[], str | None] | None = None
259
+
258
260
  def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
259
261
  if auth == "oauth":
260
- auth = OAuth(self.url)
262
+ auth = OAuth(self.url, httpx_client_factory=self.httpx_client_factory)
261
263
  elif isinstance(auth, str):
262
264
  auth = BearerAuth(auth)
263
265
  self.auth = auth
@@ -277,7 +279,7 @@ class StreamableHttpTransport(ClientTransport):
277
279
  # instead we simply leave the kwarg out if it's not provided
278
280
  if self.sse_read_timeout is not None:
279
281
  client_kwargs["sse_read_timeout"] = self.sse_read_timeout
280
- if session_kwargs.get("read_timeout_seconds", None) is not None:
282
+ if session_kwargs.get("read_timeout_seconds") is not None:
281
283
  client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
282
284
 
283
285
  if self.httpx_client_factory is not None:
@@ -288,12 +290,25 @@ class StreamableHttpTransport(ClientTransport):
288
290
  auth=self.auth,
289
291
  **client_kwargs,
290
292
  ) as transport:
291
- read_stream, write_stream, _ = transport
293
+ read_stream, write_stream, get_session_id = transport
294
+ self._get_session_id_cb = get_session_id
292
295
  async with ClientSession(
293
296
  read_stream, write_stream, **session_kwargs
294
297
  ) as session:
295
298
  yield session
296
299
 
300
+ def get_session_id(self) -> str | None:
301
+ if self._get_session_id_cb:
302
+ try:
303
+ return self._get_session_id_cb()
304
+ except Exception:
305
+ return None
306
+ return None
307
+
308
+ async def close(self):
309
+ # Reset the session id callback
310
+ self._get_session_id_cb = None
311
+
297
312
  def __repr__(self) -> str:
298
313
  return f"<StreamableHttpTransport(url='{self.url}')>"
299
314
 
@@ -313,6 +328,7 @@ class StdioTransport(ClientTransport):
313
328
  env: dict[str, str] | None = None,
314
329
  cwd: str | None = None,
315
330
  keep_alive: bool | None = None,
331
+ log_file: Path | TextIO | None = None,
316
332
  ):
317
333
  """
318
334
  Initialize a Stdio transport.
@@ -326,6 +342,11 @@ class StdioTransport(ClientTransport):
326
342
  Defaults to True. When True, the subprocess remains active
327
343
  after the connection context exits, allowing reuse in
328
344
  subsequent connections.
345
+ log_file: Optional path or file-like object where subprocess stderr will
346
+ be written. Can be a Path or TextIO object. Defaults to sys.stderr
347
+ if not provided. When a Path is provided, the file will be created
348
+ if it doesn't exist, or appended to if it does. When set, server
349
+ errors will be written to this file instead of appearing in the console.
329
350
  """
330
351
  self.command = command
331
352
  self.args = args
@@ -334,6 +355,7 @@ class StdioTransport(ClientTransport):
334
355
  if keep_alive is None:
335
356
  keep_alive = True
336
357
  self.keep_alive = keep_alive
358
+ self.log_file = log_file
337
359
 
338
360
  self._session: ClientSession | None = None
339
361
  self._connect_task: asyncio.Task | None = None
@@ -368,7 +390,9 @@ class StdioTransport(ClientTransport):
368
390
  args=self.args,
369
391
  env=self.env,
370
392
  cwd=self.cwd,
371
- session_kwargs=session_kwargs,
393
+ log_file=self.log_file,
394
+ # TODO(ty): remove when ty supports Unpack[TypedDict] inference
395
+ session_kwargs=session_kwargs, # type: ignore[arg-type]
372
396
  ready_event=self._ready_event,
373
397
  stop_event=self._stop_event,
374
398
  session_future=session_future,
@@ -421,6 +445,7 @@ async def _stdio_transport_connect_task(
421
445
  args: list[str],
422
446
  env: dict[str, str] | None,
423
447
  cwd: str | None,
448
+ log_file: Path | TextIO | None,
424
449
  session_kwargs: SessionKwargs,
425
450
  ready_event: anyio.Event,
426
451
  stop_event: anyio.Event,
@@ -438,7 +463,18 @@ async def _stdio_transport_connect_task(
438
463
  env=env,
439
464
  cwd=cwd,
440
465
  )
441
- transport = await stack.enter_async_context(stdio_client(server_params))
466
+ # Handle log_file: Path needs to be opened, TextIO used as-is
467
+ if log_file is None:
468
+ log_file_handle = sys.stderr
469
+ elif isinstance(log_file, Path):
470
+ log_file_handle = stack.enter_context(log_file.open("a"))
471
+ else:
472
+ # Must be TextIO - use it directly
473
+ log_file_handle = log_file
474
+
475
+ transport = await stack.enter_async_context(
476
+ stdio_client(server_params, errlog=log_file_handle)
477
+ )
442
478
  read_stream, write_stream = transport
443
479
  session_future.set_result(
444
480
  await stack.enter_async_context(
@@ -471,6 +507,7 @@ class PythonStdioTransport(StdioTransport):
471
507
  cwd: str | None = None,
472
508
  python_cmd: str = sys.executable,
473
509
  keep_alive: bool | None = None,
510
+ log_file: Path | TextIO | None = None,
474
511
  ):
475
512
  """
476
513
  Initialize a Python transport.
@@ -485,6 +522,11 @@ class PythonStdioTransport(StdioTransport):
485
522
  Defaults to True. When True, the subprocess remains active
486
523
  after the connection context exits, allowing reuse in
487
524
  subsequent connections.
525
+ log_file: Optional path or file-like object where subprocess stderr will
526
+ be written. Can be a Path or TextIO object. Defaults to sys.stderr
527
+ if not provided. When a Path is provided, the file will be created
528
+ if it doesn't exist, or appended to if it does. When set, server
529
+ errors will be written to this file instead of appearing in the console.
488
530
  """
489
531
  script_path = Path(script_path).resolve()
490
532
  if not script_path.is_file():
@@ -502,6 +544,7 @@ class PythonStdioTransport(StdioTransport):
502
544
  env=env,
503
545
  cwd=cwd,
504
546
  keep_alive=keep_alive,
547
+ log_file=log_file,
505
548
  )
506
549
  self.script_path = script_path
507
550
 
@@ -516,6 +559,7 @@ class FastMCPStdioTransport(StdioTransport):
516
559
  env: dict[str, str] | None = None,
517
560
  cwd: str | None = None,
518
561
  keep_alive: bool | None = None,
562
+ log_file: Path | TextIO | None = None,
519
563
  ):
520
564
  script_path = Path(script_path).resolve()
521
565
  if not script_path.is_file():
@@ -529,6 +573,7 @@ class FastMCPStdioTransport(StdioTransport):
529
573
  env=env,
530
574
  cwd=cwd,
531
575
  keep_alive=keep_alive,
576
+ log_file=log_file,
532
577
  )
533
578
  self.script_path = script_path
534
579
 
@@ -544,6 +589,7 @@ class NodeStdioTransport(StdioTransport):
544
589
  cwd: str | None = None,
545
590
  node_cmd: str = "node",
546
591
  keep_alive: bool | None = None,
592
+ log_file: Path | TextIO | None = None,
547
593
  ):
548
594
  """
549
595
  Initialize a Node transport.
@@ -558,6 +604,11 @@ class NodeStdioTransport(StdioTransport):
558
604
  Defaults to True. When True, the subprocess remains active
559
605
  after the connection context exits, allowing reuse in
560
606
  subsequent connections.
607
+ log_file: Optional path or file-like object where subprocess stderr will
608
+ be written. Can be a Path or TextIO object. Defaults to sys.stderr
609
+ if not provided. When a Path is provided, the file will be created
610
+ if it doesn't exist, or appended to if it does. When set, server
611
+ errors will be written to this file instead of appearing in the console.
561
612
  """
562
613
  script_path = Path(script_path).resolve()
563
614
  if not script_path.is_file():
@@ -570,7 +621,12 @@ class NodeStdioTransport(StdioTransport):
570
621
  full_args.extend(args)
571
622
 
572
623
  super().__init__(
573
- command=node_cmd, args=full_args, env=env, cwd=cwd, keep_alive=keep_alive
624
+ command=node_cmd,
625
+ args=full_args,
626
+ env=env,
627
+ cwd=cwd,
628
+ keep_alive=keep_alive,
629
+ log_file=log_file,
574
630
  )
575
631
  self.script_path = script_path
576
632
 
@@ -583,15 +639,15 @@ class UvStdioTransport(StdioTransport):
583
639
  command: str,
584
640
  args: list[str] | None = None,
585
641
  module: bool = False,
586
- project_directory: str | None = None,
642
+ project_directory: Path | None = None,
587
643
  python_version: str | None = None,
588
644
  with_packages: list[str] | None = None,
589
- with_requirements: str | None = None,
645
+ with_requirements: Path | None = None,
590
646
  env_vars: dict[str, str] | None = None,
591
647
  keep_alive: bool | None = None,
592
648
  ):
593
649
  # Basic validation
594
- if project_directory and not Path(project_directory).exists():
650
+ if project_directory and not project_directory.exists():
595
651
  raise NotADirectoryError(
596
652
  f"Project directory not found: {project_directory}"
597
653
  )
@@ -707,6 +763,7 @@ class UvxStdioTransport(StdioTransport):
707
763
  env: dict[str, str] | None = None
708
764
  if env_vars:
709
765
  env = os.environ.copy()
766
+ env.update(env_vars)
710
767
 
711
768
  super().__init__(
712
769
  command="uvx",
@@ -809,13 +866,25 @@ class FastMCPTransport(ClientTransport):
809
866
  client_read, client_write = client_streams
810
867
  server_read, server_write = server_streams
811
868
 
812
- # Create a cancel scope for the server task
813
- async with anyio.create_task_group() as tg:
869
+ # Capture exceptions to re-raise after task group cleanup.
870
+ # anyio task groups can suppress exceptions when cancel_scope.cancel()
871
+ # is called during cleanup, so we capture and re-raise manually.
872
+ exception_to_raise: BaseException | None = None
873
+
874
+ async with (
875
+ anyio.create_task_group() as tg,
876
+ _enter_server_lifespan(server=self.server),
877
+ ):
878
+ # Build experimental capabilities
879
+ experimental_capabilities = get_task_capabilities()
880
+
814
881
  tg.start_soon(
815
882
  lambda: self.server._mcp_server.run(
816
883
  server_read,
817
884
  server_write,
818
- self.server._mcp_server.create_initialization_options(),
885
+ self.server._mcp_server.create_initialization_options(
886
+ experimental_capabilities=experimental_capabilities
887
+ ),
819
888
  raise_exceptions=self.raise_exceptions,
820
889
  )
821
890
  )
@@ -827,13 +896,31 @@ class FastMCPTransport(ClientTransport):
827
896
  **session_kwargs,
828
897
  ) as client_session:
829
898
  yield client_session
899
+ except BaseException as e:
900
+ exception_to_raise = e
830
901
  finally:
831
902
  tg.cancel_scope.cancel()
832
903
 
904
+ # Re-raise after task group has exited cleanly
905
+ if exception_to_raise is not None:
906
+ raise exception_to_raise
907
+
833
908
  def __repr__(self) -> str:
834
909
  return f"<FastMCPTransport(server='{self.server.name}')>"
835
910
 
836
911
 
912
+ @contextlib.asynccontextmanager
913
+ async def _enter_server_lifespan(
914
+ server: FastMCP | FastMCP1Server,
915
+ ) -> AsyncIterator[None]:
916
+ """Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers."""
917
+ if isinstance(server, FastMCP):
918
+ async with server._lifespan_manager():
919
+ yield
920
+ else:
921
+ yield
922
+
923
+
837
924
  class MCPConfigTransport(ClientTransport):
838
925
  """Transport for connecting to one or more MCP servers defined in an MCPConfig.
839
926
 
@@ -897,7 +984,7 @@ class MCPConfigTransport(ClientTransport):
897
984
 
898
985
  # if there's exactly one server, create a client for that server
899
986
  elif len(self.config.mcpServers) == 1:
900
- self.transport = list(self.config.mcpServers.values())[0].to_transport()
987
+ self.transport = next(iter(self.config.mcpServers.values())).to_transport()
901
988
  self._underlying_transports.append(self.transport)
902
989
 
903
990
  # otherwise create a composite client
@@ -1,4 +1,4 @@
1
1
  from .component_manager import set_up_component_manager
2
2
  from .component_service import ComponentService
3
3
 
4
- __all__ = ["set_up_component_manager", "ComponentService"]
4
+ __all__ = ["ComponentService", "set_up_component_manager"]
@@ -97,11 +97,11 @@ def make_endpoint(action, component, config):
97
97
  return JSONResponse(
98
98
  {"message": f"{action.capitalize()}d {component}: {name}"}
99
99
  )
100
- except NotFoundError:
100
+ except NotFoundError as e:
101
101
  raise StarletteHTTPException(
102
102
  status_code=404,
103
103
  detail=f"Unknown {component}: {name}",
104
- )
104
+ ) from e
105
105
 
106
106
  return endpoint
107
107
 
@@ -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,18 +103,10 @@ 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
- if has_resource_prefix(
109
- key,
110
- mounted.prefix,
111
- mounted.resource_prefix_format,
112
- ):
113
- key = remove_resource_prefix(
114
- key,
115
- mounted.prefix,
116
- mounted.resource_prefix_format,
117
- )
108
+ if has_resource_prefix(key, mounted.prefix):
109
+ key = remove_resource_prefix(key, mounted.prefix)
118
110
  mounted_service = ComponentService(mounted.server)
119
111
  mounted_resource: (
120
112
  Resource | ResourceTemplate
@@ -146,18 +138,10 @@ class ComponentService:
146
138
  return template
147
139
 
148
140
  # 2. Check mounted servers using the filtered protocol path.
149
- for mounted in reversed(self._resource_manager._mounted_servers):
141
+ for mounted in reversed(self._server._mounted_servers):
150
142
  if mounted.prefix:
151
- if has_resource_prefix(
152
- key,
153
- mounted.prefix,
154
- mounted.resource_prefix_format,
155
- ):
156
- key = remove_resource_prefix(
157
- key,
158
- mounted.prefix,
159
- mounted.resource_prefix_format,
160
- )
143
+ if has_resource_prefix(key, mounted.prefix):
144
+ key = remove_resource_prefix(key, mounted.prefix)
161
145
  mounted_service = ComponentService(mounted.server)
162
146
  mounted_resource: (
163
147
  Resource | ResourceTemplate
@@ -185,7 +169,7 @@ class ComponentService:
185
169
  return prompt
186
170
 
187
171
  # 2. Check mounted servers using the filtered protocol path.
188
- for mounted in reversed(self._prompt_manager._mounted_servers):
172
+ for mounted in reversed(self._server._mounted_servers):
189
173
  if mounted.prefix:
190
174
  if key.startswith(f"{mounted.prefix}_"):
191
175
  prompt_key = key.removeprefix(f"{mounted.prefix}_")
@@ -213,7 +197,7 @@ class ComponentService:
213
197
  return prompt
214
198
 
215
199
  # 2. Check mounted servers using the filtered protocol path.
216
- for mounted in reversed(self._prompt_manager._mounted_servers):
200
+ for mounted in reversed(self._server._mounted_servers):
217
201
  if mounted.prefix:
218
202
  if key.startswith(f"{mounted.prefix}_"):
219
203
  prompt_key = key.removeprefix(f"{mounted.prefix}_")
@@ -11,12 +11,15 @@ Tools:
11
11
  * [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)
12
12
  * [annotations](https://gofastmcp.com/servers/tools#annotations-2)
13
13
  * [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)
14
+ * [meta](https://gofastmcp.com/servers/tools#param-meta)
14
15
 
15
16
  Prompts:
16
17
  * [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)
18
+ * [meta](https://gofastmcp.com/servers/prompts#param-meta)
17
19
 
18
20
  Resources:
19
21
  * [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources)
22
+ * [meta](https://gofastmcp.com/servers/resources#param-meta)
20
23
 
21
24
  ## Usage
22
25
 
@@ -78,7 +81,16 @@ class MyComponent(MCPMixin):
78
81
  if delete_all:
79
82
  return "99 records deleted. I bet you're not a tool :)"
80
83
  return "Tool executed, but you might be a tool!"
81
-
84
+
85
+ # example tool w/ meta
86
+ @mcp_tool(
87
+ name="data_tool",
88
+ description="Fetches user data from database",
89
+ meta={"version": "2.0", "category": "database", "author": "dev-team"}
90
+ )
91
+ def data_tool_method(self, user_id: int):
92
+ return f"Fetching data for user {user_id}"
93
+
82
94
  @mcp_resource(uri="component://data")
83
95
  def resource_method(self):
84
96
  return {"data": "some data"}
@@ -88,6 +100,15 @@ class MyComponent(MCPMixin):
88
100
  def resource_method(self):
89
101
  return {"data": "some data"}
90
102
 
103
+ # example resource w/meta and title
104
+ @mcp_resource(
105
+ uri="component://config",
106
+ title="Data resource Title,
107
+ meta={"internal": True, "cache_ttl": 3600, "priority": "high"}
108
+ )
109
+ def config_resource_method(self):
110
+ return {"config": "data"}
111
+
91
112
  # prompt
92
113
  @mcp_prompt(name="A prompt")
93
114
  def prompt_method(self, name):
@@ -98,6 +119,16 @@ class MyComponent(MCPMixin):
98
119
  def prompt_method(self, name):
99
120
  return f"What's up {name}?"
100
121
 
122
+ # example prompt w/title and meta
123
+ @mcp_prompt(
124
+ name="analysis_prompt",
125
+ title="Data Analysis Prompt",
126
+ description="Analyzes data patterns",
127
+ meta={"complexity": "high", "domain": "analytics", "requires_context": True}
128
+ )
129
+ def analysis_prompt_method(self, dataset: str):
130
+ return f"Analyze the patterns in {dataset}"
131
+
101
132
  mcp_server = FastMCP()
102
133
  component = MyComponent()
103
134
 
@@ -2,7 +2,7 @@ from .mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt
2
2
 
3
3
  __all__ = [
4
4
  "MCPMixin",
5
- "mcp_tool",
6
- "mcp_resource",
7
5
  "mcp_prompt",
6
+ "mcp_resource",
7
+ "mcp_tool",
8
8
  ]
@@ -3,7 +3,7 @@
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from mcp.types import ToolAnnotations
6
+ from mcp.types import Annotations, ToolAnnotations
7
7
 
8
8
  from fastmcp.prompts.prompt import Prompt
9
9
  from fastmcp.resources.resource import Resource
@@ -29,6 +29,7 @@ def mcp_tool(
29
29
  annotations: ToolAnnotations | dict[str, Any] | None = None,
30
30
  exclude_args: list[str] | None = None,
31
31
  serializer: Callable[[Any], str] | None = None,
32
+ meta: dict[str, Any] | None = None,
32
33
  enabled: bool | None = None,
33
34
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
34
35
  """Decorator to mark a method as an MCP tool for later registration."""
@@ -41,6 +42,7 @@ def mcp_tool(
41
42
  "annotations": annotations,
42
43
  "exclude_args": exclude_args,
43
44
  "serializer": serializer,
45
+ "meta": meta,
44
46
  "enabled": enabled,
45
47
  }
46
48
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -54,9 +56,12 @@ def mcp_resource(
54
56
  uri: str,
55
57
  *,
56
58
  name: str | None = None,
59
+ title: str | None = None,
57
60
  description: str | None = None,
58
61
  mime_type: str | None = None,
59
62
  tags: set[str] | None = None,
63
+ annotations: Annotations | None = None,
64
+ meta: dict[str, Any] | None = None,
60
65
  enabled: bool | None = None,
61
66
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
62
67
  """Decorator to mark a method as an MCP resource for later registration."""
@@ -65,9 +70,12 @@ def mcp_resource(
65
70
  call_args = {
66
71
  "uri": uri,
67
72
  "name": name or get_fn_name(func),
73
+ "title": title,
68
74
  "description": description,
69
75
  "mime_type": mime_type,
70
76
  "tags": tags,
77
+ "annotations": annotations,
78
+ "meta": meta,
71
79
  "enabled": enabled,
72
80
  }
73
81
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -81,8 +89,10 @@ def mcp_resource(
81
89
 
82
90
  def mcp_prompt(
83
91
  name: str | None = None,
92
+ title: str | None = None,
84
93
  description: str | None = None,
85
94
  tags: set[str] | None = None,
95
+ meta: dict[str, Any] | None = None,
86
96
  enabled: bool | None = None,
87
97
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
88
98
  """Decorator to mark a method as an MCP prompt for later registration."""
@@ -90,8 +100,10 @@ def mcp_prompt(
90
100
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
91
101
  call_args = {
92
102
  "name": name or get_fn_name(func),
103
+ "title": title,
93
104
  "description": description,
94
105
  "tags": tags,
106
+ "meta": meta,
95
107
  "enabled": enabled,
96
108
  }
97
109
 
@@ -151,7 +163,6 @@ class MCPMixin:
151
163
  tool = Tool.from_function(
152
164
  fn=method,
153
165
  name=registration_info.get("name"),
154
- title=registration_info.get("title"),
155
166
  description=registration_info.get("description"),
156
167
  tags=registration_info.get("tags"),
157
168
  annotations=registration_info.get("annotations"),
@@ -195,6 +206,7 @@ class MCPMixin:
195
206
  fn=method,
196
207
  uri=registration_info["uri"],
197
208
  name=registration_info.get("name"),
209
+ title=registration_info.get("title"),
198
210
  description=registration_info.get("description"),
199
211
  mime_type=registration_info.get("mime_type"),
200
212
  tags=registration_info.get("tags"),
@@ -0,0 +1,25 @@
1
+ """Dependency injection exports for FastMCP.
2
+
3
+ This module re-exports dependency injection symbols from Docket and FastMCP
4
+ to provide a clean, centralized import location for all dependency-related
5
+ functionality.
6
+ """
7
+
8
+ from docket import Depends
9
+
10
+ from fastmcp.server.dependencies import (
11
+ CurrentContext,
12
+ CurrentDocket,
13
+ CurrentFastMCP,
14
+ CurrentWorker,
15
+ Progress,
16
+ )
17
+
18
+ __all__ = [
19
+ "CurrentContext",
20
+ "CurrentDocket",
21
+ "CurrentFastMCP",
22
+ "CurrentWorker",
23
+ "Depends",
24
+ "Progress",
25
+ ]