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
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from enum import Enum
5
- from typing import Any, Generic, Literal, get_origin
5
+ from typing import Any, Generic, Literal, cast, get_origin
6
6
 
7
7
  from mcp.server.elicitation import (
8
8
  CancelledElicitation,
@@ -47,10 +47,10 @@ class ElicitationJsonSchema(GenerateJsonSchema):
47
47
  if schema["type"] == "enum":
48
48
  # Directly call our custom enum_schema without going through handler
49
49
  # This prevents the ref/defs mechanism from being invoked
50
- return self.enum_schema(schema) # type: ignore[arg-type]
50
+ return self.enum_schema(schema)
51
51
  # For list schemas, check if items are enums
52
52
  if schema["type"] == "list":
53
- return self.list_schema(schema) # type: ignore[arg-type]
53
+ return self.list_schema(schema)
54
54
  # For all other types, use the default implementation
55
55
  return super().generate_inner(schema)
56
56
 
@@ -94,7 +94,7 @@ class ElicitationJsonSchema(GenerateJsonSchema):
94
94
  def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
95
95
  """Generate inline enum schema.
96
96
 
97
- Always generates enum pattern: {"enum": [value, ...]}
97
+ Always generates enum pattern: `{"enum": [value, ...]}`
98
98
  Titled enums are handled separately via dict-based syntax in ctx.elicit().
99
99
  """
100
100
  # Get the base schema from parent - always use simple enum pattern
@@ -134,12 +134,12 @@ def parse_elicit_response_type(response_type: Any) -> ElicitConfig:
134
134
 
135
135
  Supports multiple syntaxes:
136
136
  - None: Empty object schema, expect empty response
137
- - dict: {"low": {"title": "..."}} -> single-select titled enum
137
+ - dict: `{"low": {"title": "..."}}` -> single-select titled enum
138
138
  - list patterns:
139
- - [["a", "b"]] -> multi-select untitled
140
- - [{"low": {...}}] -> multi-select titled
141
- - ["a", "b"] -> single-select untitled
142
- - list[X] type annotation: multi-select with type
139
+ - `[["a", "b"]]` -> multi-select untitled
140
+ - `[{"low": {...}}]` -> multi-select titled
141
+ - `["a", "b"]` -> single-select untitled
142
+ - `list[X]` type annotation: multi-select with type
143
143
  - Scalar types (bool, int, float, str, Literal, Enum): single value
144
144
  - Other types (dataclass, BaseModel): use directly
145
145
  """
@@ -229,11 +229,13 @@ def _parse_list_syntax(lst: list[Any]) -> ElicitConfig:
229
229
 
230
230
  # ["a", "b", "c"] -> single-select untitled
231
231
  if lst and all(isinstance(item, str) for item in lst):
232
- choice_literal = Literal[tuple(lst)] # type: ignore[valid-type]
232
+ # Construct Literal type from tuple - use cast since we can't construct Literal dynamically
233
+ # but we know the values are all strings
234
+ choice_literal: type[Any] = cast(type[Any], Literal[tuple(lst)]) # type: ignore[valid-type]
233
235
  wrapped = ScalarElicitationType[choice_literal] # type: ignore[valid-type]
234
236
  return ElicitConfig(
235
- schema=get_elicitation_schema(wrapped), # type: ignore[arg-type]
236
- response_type=wrapped, # type: ignore[assignment]
237
+ schema=get_elicitation_schema(wrapped),
238
+ response_type=wrapped,
237
239
  is_raw=False,
238
240
  )
239
241
 
@@ -242,20 +244,20 @@ def _parse_list_syntax(lst: list[Any]) -> ElicitConfig:
242
244
 
243
245
  def _parse_generic_list(response_type: Any) -> ElicitConfig:
244
246
  """Parse list[X] type annotation -> multi-select."""
245
- wrapped = ScalarElicitationType[response_type] # type: ignore[valid-type]
247
+ wrapped = ScalarElicitationType[response_type]
246
248
  return ElicitConfig(
247
- schema=get_elicitation_schema(wrapped), # type: ignore[arg-type]
248
- response_type=wrapped, # type: ignore[assignment]
249
+ schema=get_elicitation_schema(wrapped),
250
+ response_type=wrapped,
249
251
  is_raw=False,
250
252
  )
251
253
 
252
254
 
253
255
  def _parse_scalar_type(response_type: Any) -> ElicitConfig:
254
256
  """Parse scalar types (bool, int, float, str, Literal, Enum)."""
255
- wrapped = ScalarElicitationType[response_type] # type: ignore[valid-type]
257
+ wrapped = ScalarElicitationType[response_type]
256
258
  return ElicitConfig(
257
- schema=get_elicitation_schema(wrapped), # type: ignore[arg-type]
258
- response_type=wrapped, # type: ignore[assignment]
259
+ schema=get_elicitation_schema(wrapped),
260
+ response_type=wrapped,
259
261
  is_raw=False,
260
262
  )
261
263
 
@@ -16,14 +16,14 @@ from key_value.aio.stores.memory import MemoryStore
16
16
  from mcp.server.streamable_http import EventCallback, EventId, EventMessage, StreamId
17
17
  from mcp.server.streamable_http import EventStore as SDKEventStore
18
18
  from mcp.types import JSONRPCMessage
19
- from pydantic import BaseModel
20
19
 
21
20
  from fastmcp.utilities.logging import get_logger
21
+ from fastmcp.utilities.types import FastMCPBaseModel
22
22
 
23
23
  logger = get_logger(__name__)
24
24
 
25
25
 
26
- class EventEntry(BaseModel):
26
+ class EventEntry(FastMCPBaseModel):
27
27
  """Stored event entry."""
28
28
 
29
29
  event_id: str
@@ -31,7 +31,7 @@ class EventEntry(BaseModel):
31
31
  message: dict | None # JSONRPCMessage serialized to dict
32
32
 
33
33
 
34
- class StreamEventList(BaseModel):
34
+ class StreamEventList(FastMCPBaseModel):
35
35
  """List of event IDs for a stream."""
36
36
 
37
37
  event_ids: list[str]
fastmcp/server/http.py CHANGED
@@ -61,7 +61,7 @@ class StreamableHTTPASGIApp:
61
61
  raise
62
62
 
63
63
 
64
- _current_http_request: ContextVar[Request | None] = ContextVar( # type: ignore[assignment]
64
+ _current_http_request: ContextVar[Request | None] = ContextVar(
65
65
  "http_request",
66
66
  default=None,
67
67
  )
@@ -84,7 +84,7 @@ def set_http_request(request: Request) -> Generator[Request, None, None]:
84
84
 
85
85
  class RequestContextMiddleware:
86
86
  """
87
- Middleware that stores each request in a ContextVar
87
+ Middleware that stores each request in a ContextVar and sets transport type.
88
88
  """
89
89
 
90
90
  def __init__(self, app):
@@ -92,8 +92,17 @@ class RequestContextMiddleware:
92
92
 
93
93
  async def __call__(self, scope, receive, send):
94
94
  if scope["type"] == "http":
95
- with set_http_request(Request(scope)):
96
- await self.app(scope, receive, send)
95
+ from fastmcp.server.context import reset_transport, set_transport
96
+
97
+ # Get transport type from app state (set during app creation)
98
+ transport_type = getattr(scope["app"].state, "transport_type", None)
99
+ transport_token = set_transport(transport_type) if transport_type else None
100
+ try:
101
+ with set_http_request(Request(scope)):
102
+ await self.app(scope, receive, send)
103
+ finally:
104
+ if transport_token is not None:
105
+ reset_transport(transport_token)
97
106
  else:
98
107
  await self.app(scope, receive, send)
99
108
 
@@ -209,7 +218,7 @@ def create_sse_app(
209
218
  else:
210
219
  # No auth required
211
220
  async def sse_endpoint(request: Request) -> Response:
212
- return await handle_sse(request.scope, request.receive, request._send) # type: ignore[reportPrivateUsage]
221
+ return await handle_sse(request.scope, request.receive, request._send)
213
222
 
214
223
  server_routes.append(
215
224
  Route(
@@ -249,6 +258,7 @@ def create_sse_app(
249
258
  # Store the FastMCP server instance on the Starlette app state
250
259
  app.state.fastmcp_server = server
251
260
  app.state.path = sse_path
261
+ app.state.transport_type = "sse"
252
262
 
253
263
  return app
254
264
 
@@ -360,7 +370,7 @@ def create_streamable_http_app(
360
370
  )
361
371
  # Store the FastMCP server instance on the Starlette app state
362
372
  app.state.fastmcp_server = server
363
-
364
373
  app.state.path = streamable_http_path
374
+ app.state.transport_type = "streamable-http"
365
375
 
366
376
  return app
@@ -0,0 +1,198 @@
1
+ """Composable lifespans for FastMCP servers.
2
+
3
+ This module provides a `@lifespan` decorator for creating composable server lifespans
4
+ that can be combined using the `|` operator.
5
+
6
+ Example:
7
+ ```python
8
+ from fastmcp import FastMCP
9
+ from fastmcp.server.lifespan import lifespan
10
+
11
+ @lifespan
12
+ async def db_lifespan(server):
13
+ conn = await connect_db()
14
+ yield {"db": conn}
15
+ await conn.close()
16
+
17
+ @lifespan
18
+ async def cache_lifespan(server):
19
+ cache = await connect_cache()
20
+ yield {"cache": cache}
21
+ await cache.close()
22
+
23
+ mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan)
24
+ ```
25
+
26
+ To compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:
27
+
28
+ ```python
29
+ from contextlib import asynccontextmanager
30
+ from fastmcp.server.lifespan import lifespan, ContextManagerLifespan
31
+
32
+ @asynccontextmanager
33
+ async def legacy_lifespan(server):
34
+ yield {"legacy": True}
35
+
36
+ @lifespan
37
+ async def new_lifespan(server):
38
+ yield {"new": True}
39
+
40
+ # Wrap the legacy lifespan explicitly
41
+ combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan
42
+ ```
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from collections.abc import AsyncIterator, Callable
48
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
49
+ from typing import TYPE_CHECKING, Any
50
+
51
+ if TYPE_CHECKING:
52
+ from fastmcp.server.server import FastMCP
53
+
54
+
55
+ LifespanFn = Callable[["FastMCP[Any]"], AsyncIterator[dict[str, Any] | None]]
56
+ LifespanContextManagerFn = Callable[
57
+ ["FastMCP[Any]"], AbstractAsyncContextManager[dict[str, Any] | None]
58
+ ]
59
+
60
+
61
+ class Lifespan:
62
+ """Composable lifespan wrapper.
63
+
64
+ Wraps an async generator function and enables composition via the `|` operator.
65
+ The wrapped function should yield a dict that becomes part of the lifespan context.
66
+ """
67
+
68
+ def __init__(self, fn: LifespanFn) -> None:
69
+ """Initialize a Lifespan wrapper.
70
+
71
+ Args:
72
+ fn: An async generator function that takes a FastMCP server and yields
73
+ a dict for the lifespan context.
74
+ """
75
+ self._fn = fn
76
+
77
+ @asynccontextmanager
78
+ async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:
79
+ """Execute the lifespan as an async context manager.
80
+
81
+ Args:
82
+ server: The FastMCP server instance.
83
+
84
+ Yields:
85
+ The lifespan context dict.
86
+ """
87
+ async with asynccontextmanager(self._fn)(server) as result:
88
+ yield result if result is not None else {}
89
+
90
+ def __or__(self, other: Lifespan) -> ComposedLifespan:
91
+ """Compose with another lifespan using the | operator.
92
+
93
+ Args:
94
+ other: Another Lifespan instance.
95
+
96
+ Returns:
97
+ A ComposedLifespan that runs both lifespans.
98
+
99
+ Raises:
100
+ TypeError: If other is not a Lifespan instance.
101
+ """
102
+ if not isinstance(other, Lifespan):
103
+ raise TypeError(
104
+ f"Cannot compose Lifespan with {type(other).__name__}. "
105
+ f"Use @lifespan decorator or wrap with ContextManagerLifespan()."
106
+ )
107
+ return ComposedLifespan(self, other)
108
+
109
+
110
+ class ContextManagerLifespan(Lifespan):
111
+ """Lifespan wrapper for already-wrapped context manager functions.
112
+
113
+ Use this for functions already decorated with @asynccontextmanager.
114
+ """
115
+
116
+ _fn: LifespanContextManagerFn # Override type for this subclass
117
+
118
+ def __init__(self, fn: LifespanContextManagerFn) -> None:
119
+ """Initialize with a context manager factory function."""
120
+ self._fn = fn
121
+
122
+ @asynccontextmanager
123
+ async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:
124
+ """Execute the lifespan as an async context manager.
125
+
126
+ Args:
127
+ server: The FastMCP server instance.
128
+
129
+ Yields:
130
+ The lifespan context dict.
131
+ """
132
+ # self._fn is already a context manager factory, just call it
133
+ async with self._fn(server) as result:
134
+ yield result if result is not None else {}
135
+
136
+
137
+ class ComposedLifespan(Lifespan):
138
+ """Two lifespans composed together.
139
+
140
+ Enters the left lifespan first, then the right. Exits in reverse order.
141
+ Results are shallow-merged into a single dict.
142
+ """
143
+
144
+ def __init__(self, left: Lifespan, right: Lifespan) -> None:
145
+ """Initialize a composed lifespan.
146
+
147
+ Args:
148
+ left: The first lifespan to enter.
149
+ right: The second lifespan to enter.
150
+ """
151
+ # Don't call super().__init__ since we override __call__
152
+ self._left = left
153
+ self._right = right
154
+
155
+ @asynccontextmanager
156
+ async def __call__(self, server: FastMCP[Any]) -> AsyncIterator[dict[str, Any]]:
157
+ """Execute both lifespans, merging their results.
158
+
159
+ Args:
160
+ server: The FastMCP server instance.
161
+
162
+ Yields:
163
+ The merged lifespan context dict from both lifespans.
164
+ """
165
+ async with (
166
+ self._left(server) as left_result,
167
+ self._right(server) as right_result,
168
+ ):
169
+ yield {**left_result, **right_result}
170
+
171
+
172
+ def lifespan(fn: LifespanFn) -> Lifespan:
173
+ """Decorator to create a composable lifespan.
174
+
175
+ Use this decorator on an async generator function to make it composable
176
+ with other lifespans using the `|` operator.
177
+
178
+ Example:
179
+ ```python
180
+ @lifespan
181
+ async def my_lifespan(server):
182
+ # Setup
183
+ resource = await create_resource()
184
+ yield {"resource": resource}
185
+ # Teardown
186
+ await resource.close()
187
+
188
+ mcp = FastMCP("server", lifespan=my_lifespan | other_lifespan)
189
+ ```
190
+
191
+ Args:
192
+ fn: An async generator function that takes a FastMCP server and yields
193
+ a dict for the lifespan context.
194
+
195
+ Returns:
196
+ A composable Lifespan wrapper.
197
+ """
198
+ return Lifespan(fn)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import weakref
4
+ from collections.abc import Awaitable, Callable
4
5
  from contextlib import AsyncExitStack
5
6
  from typing import TYPE_CHECKING, Any
6
7
 
@@ -21,6 +22,7 @@ from mcp.server.session import ServerSession
21
22
  from mcp.server.stdio import stdio_server as stdio_server
22
23
  from mcp.shared.message import SessionMessage
23
24
  from mcp.shared.session import RequestResponder
25
+ from pydantic import AnyUrl
24
26
 
25
27
  from fastmcp.utilities.logging import get_logger
26
28
 
@@ -94,7 +96,7 @@ class MiddlewareServerSession(ServerSession):
94
96
  return None
95
97
 
96
98
  async with fastmcp.server.context.Context(
97
- fastmcp=self.fastmcp
99
+ fastmcp=self.fastmcp, session=self
98
100
  ) as fastmcp_ctx:
99
101
  # Create the middleware context.
100
102
  mw_context = MiddlewareContext(
@@ -106,7 +108,7 @@ class MiddlewareServerSession(ServerSession):
106
108
  )
107
109
 
108
110
  try:
109
- return await self.fastmcp._apply_middleware(
111
+ return await self.fastmcp._run_middleware(
110
112
  mw_context, call_original_handler
111
113
  )
112
114
  except McpError as e:
@@ -223,3 +225,91 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
223
225
  lifespan_context,
224
226
  raise_exceptions,
225
227
  )
228
+
229
+ def read_resource(
230
+ self,
231
+ ) -> Callable[
232
+ [
233
+ Callable[
234
+ [AnyUrl],
235
+ Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
236
+ ]
237
+ ],
238
+ Callable[
239
+ [AnyUrl],
240
+ Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
241
+ ],
242
+ ]:
243
+ """
244
+ Decorator for registering a read_resource handler with CreateTaskResult support.
245
+
246
+ The MCP SDK's read_resource decorator does not support returning CreateTaskResult
247
+ for background task execution. This decorator wraps the result in ServerResult.
248
+
249
+ This decorator can be removed once the MCP SDK adds native CreateTaskResult support
250
+ for resources.
251
+ """
252
+
253
+ def decorator(
254
+ func: Callable[
255
+ [AnyUrl],
256
+ Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
257
+ ],
258
+ ) -> Callable[
259
+ [AnyUrl],
260
+ Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult],
261
+ ]:
262
+ async def handler(
263
+ req: mcp.types.ReadResourceRequest,
264
+ ) -> mcp.types.ServerResult:
265
+ result = await func(req.params.uri)
266
+ return mcp.types.ServerResult(result)
267
+
268
+ self.request_handlers[mcp.types.ReadResourceRequest] = handler
269
+ return func
270
+
271
+ return decorator
272
+
273
+ def get_prompt(
274
+ self,
275
+ ) -> Callable[
276
+ [
277
+ Callable[
278
+ [str, dict[str, Any] | None],
279
+ Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
280
+ ]
281
+ ],
282
+ Callable[
283
+ [str, dict[str, Any] | None],
284
+ Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
285
+ ],
286
+ ]:
287
+ """
288
+ Decorator for registering a get_prompt handler with CreateTaskResult support.
289
+
290
+ The MCP SDK's get_prompt decorator does not support returning CreateTaskResult
291
+ for background task execution. This decorator wraps the result in ServerResult.
292
+
293
+ This decorator can be removed once the MCP SDK adds native CreateTaskResult support
294
+ for prompts.
295
+ """
296
+
297
+ def decorator(
298
+ func: Callable[
299
+ [str, dict[str, Any] | None],
300
+ Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
301
+ ],
302
+ ) -> Callable[
303
+ [str, dict[str, Any] | None],
304
+ Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult],
305
+ ]:
306
+ async def handler(
307
+ req: mcp.types.GetPromptRequest,
308
+ ) -> mcp.types.ServerResult:
309
+ result = await func(req.params.name, req.params.arguments)
310
+ return mcp.types.ServerResult(result)
311
+
312
+ self.request_handlers[mcp.types.GetPromptRequest] = handler
313
+ return func
314
+
315
+ return decorator
@@ -1,11 +1,15 @@
1
+ from .authorization import AuthMiddleware
1
2
  from .middleware import (
3
+ CallNext,
2
4
  Middleware,
3
5
  MiddlewareContext,
4
- CallNext,
5
6
  )
7
+ from .ping import PingMiddleware
6
8
 
7
9
  __all__ = [
10
+ "AuthMiddleware",
8
11
  "CallNext",
9
12
  "Middleware",
10
13
  "MiddlewareContext",
14
+ "PingMiddleware",
11
15
  ]