fastmcp 2.11.2__py3-none-any.whl → 2.12.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 (77) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/logging.py +25 -1
  13. fastmcp/client/oauth_callback.py +91 -91
  14. fastmcp/client/sampling.py +12 -4
  15. fastmcp/client/transports.py +143 -67
  16. fastmcp/experimental/sampling/__init__.py +0 -0
  17. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  18. fastmcp/experimental/sampling/handlers/base.py +21 -0
  19. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  20. fastmcp/experimental/server/openapi/routing.py +1 -3
  21. fastmcp/experimental/server/openapi/server.py +10 -25
  22. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  23. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  24. fastmcp/experimental/utilities/openapi/models.py +5 -2
  25. fastmcp/experimental/utilities/openapi/parser.py +252 -70
  26. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  27. fastmcp/mcp_config.py +40 -20
  28. fastmcp/prompts/prompt_manager.py +4 -2
  29. fastmcp/resources/resource_manager.py +16 -6
  30. fastmcp/server/auth/__init__.py +11 -1
  31. fastmcp/server/auth/auth.py +19 -2
  32. fastmcp/server/auth/oauth_proxy.py +1047 -0
  33. fastmcp/server/auth/providers/azure.py +270 -0
  34. fastmcp/server/auth/providers/github.py +287 -0
  35. fastmcp/server/auth/providers/google.py +305 -0
  36. fastmcp/server/auth/providers/jwt.py +27 -16
  37. fastmcp/server/auth/providers/workos.py +256 -2
  38. fastmcp/server/auth/redirect_validation.py +65 -0
  39. fastmcp/server/auth/registry.py +1 -1
  40. fastmcp/server/context.py +91 -41
  41. fastmcp/server/dependencies.py +32 -2
  42. fastmcp/server/elicitation.py +60 -1
  43. fastmcp/server/http.py +44 -37
  44. fastmcp/server/middleware/logging.py +66 -28
  45. fastmcp/server/proxy.py +2 -0
  46. fastmcp/server/sampling/handler.py +19 -0
  47. fastmcp/server/server.py +85 -20
  48. fastmcp/settings.py +18 -3
  49. fastmcp/tools/tool.py +23 -10
  50. fastmcp/tools/tool_manager.py +5 -1
  51. fastmcp/tools/tool_transform.py +75 -32
  52. fastmcp/utilities/auth.py +34 -0
  53. fastmcp/utilities/cli.py +148 -15
  54. fastmcp/utilities/components.py +21 -5
  55. fastmcp/utilities/inspect.py +166 -37
  56. fastmcp/utilities/json_schema_type.py +4 -2
  57. fastmcp/utilities/logging.py +4 -1
  58. fastmcp/utilities/mcp_config.py +47 -18
  59. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  60. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  61. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  62. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  63. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  64. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  65. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  66. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  67. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  68. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  69. fastmcp/utilities/openapi.py +4 -4
  70. fastmcp/utilities/tests.py +7 -2
  71. fastmcp/utilities/types.py +15 -2
  72. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0.dist-info/RECORD +129 -0
  74. fastmcp-2.11.2.dist-info/RECORD +0 -108
  75. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  77. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,65 @@
1
+ """Utilities for validating client redirect URIs in OAuth flows."""
2
+
3
+ import fnmatch
4
+
5
+ from pydantic import AnyUrl
6
+
7
+
8
+ def matches_allowed_pattern(uri: str, pattern: str) -> bool:
9
+ """Check if a URI matches an allowed pattern with wildcard support.
10
+
11
+ Patterns support * wildcard matching:
12
+ - http://localhost:* matches any localhost port
13
+ - http://127.0.0.1:* matches any 127.0.0.1 port
14
+ - https://*.example.com/* matches any subdomain of example.com
15
+ - https://app.example.com/auth/* matches any path under /auth/
16
+
17
+ Args:
18
+ uri: The redirect URI to validate
19
+ pattern: The allowed pattern (may contain wildcards)
20
+
21
+ Returns:
22
+ True if the URI matches the pattern
23
+ """
24
+ # Use fnmatch for wildcard matching
25
+ return fnmatch.fnmatch(uri, pattern)
26
+
27
+
28
+ def validate_redirect_uri(
29
+ redirect_uri: str | AnyUrl | None,
30
+ allowed_patterns: list[str] | None,
31
+ ) -> bool:
32
+ """Validate a redirect URI against allowed patterns.
33
+
34
+ Args:
35
+ redirect_uri: The redirect URI to validate
36
+ allowed_patterns: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility).
37
+ If empty list, no URIs are allowed.
38
+ To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS.
39
+
40
+ Returns:
41
+ True if the redirect URI is allowed
42
+ """
43
+ if redirect_uri is None:
44
+ return True # None is allowed (will use client's default)
45
+
46
+ uri_str = str(redirect_uri)
47
+
48
+ # If no patterns specified, allow all for DCR compatibility
49
+ # (clients need to dynamically register with their own redirect URIs)
50
+ if allowed_patterns is None:
51
+ return True
52
+
53
+ # Check if URI matches any allowed pattern
54
+ for pattern in allowed_patterns:
55
+ if matches_allowed_pattern(uri_str, pattern):
56
+ return True
57
+
58
+ return False
59
+
60
+
61
+ # Default patterns for localhost-only validation
62
+ DEFAULT_LOCALHOST_PATTERNS = [
63
+ "http://localhost:*",
64
+ "http://127.0.0.1:*",
65
+ ]
@@ -6,7 +6,7 @@ from collections.abc import Callable
6
6
  from typing import TYPE_CHECKING, TypeVar
7
7
 
8
8
  if TYPE_CHECKING:
9
- from fastmcp.server.auth.auth import AuthProvider
9
+ from fastmcp.server.auth import AuthProvider
10
10
 
11
11
  # Type variable for auth providers
12
12
  T = TypeVar("T", bound="AuthProvider")
fastmcp/server/context.py CHANGED
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import copy
5
+ import inspect
5
6
  import warnings
7
+ import weakref
6
8
  from collections.abc import Generator, Mapping
7
9
  from contextlib import contextmanager
8
10
  from contextvars import ContextVar, Token
@@ -15,15 +17,18 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
15
17
  from mcp.server.lowlevel.server import request_ctx
16
18
  from mcp.shared.context import RequestContext
17
19
  from mcp.types import (
20
+ ClientCapabilities,
18
21
  ContentBlock,
19
22
  CreateMessageResult,
20
23
  IncludeContext,
21
24
  ModelHint,
22
25
  ModelPreferences,
23
26
  Root,
27
+ SamplingCapability,
24
28
  SamplingMessage,
25
29
  TextContent,
26
30
  )
31
+ from mcp.types import CreateMessageRequestParams as SamplingParams
27
32
  from pydantic.networks import AnyUrl
28
33
  from starlette.requests import Request
29
34
 
@@ -43,7 +48,7 @@ from fastmcp.utilities.types import get_cached_typeadapter
43
48
  logger = get_logger(__name__)
44
49
 
45
50
  T = TypeVar("T")
46
- _current_context: ContextVar[Context | None] = ContextVar("context", default=None)
51
+ _current_context: ContextVar[Context | None] = ContextVar("context", default=None) # type: ignore[assignment]
47
52
  _flush_lock = asyncio.Lock()
48
53
 
49
54
 
@@ -115,11 +120,19 @@ class Context:
115
120
  """
116
121
 
117
122
  def __init__(self, fastmcp: FastMCP):
118
- self.fastmcp = fastmcp
123
+ self._fastmcp: weakref.ref[FastMCP] = weakref.ref(fastmcp)
119
124
  self._tokens: list[Token] = []
120
125
  self._notification_queue: set[str] = set() # Dedupe notifications
121
126
  self._state: dict[str, Any] = {}
122
127
 
128
+ @property
129
+ def fastmcp(self) -> FastMCP:
130
+ """Get the FastMCP instance."""
131
+ fastmcp = self._fastmcp()
132
+ if fastmcp is None:
133
+ raise RuntimeError("FastMCP instance is no longer available")
134
+ return fastmcp
135
+
123
136
  async def __aenter__(self) -> Context:
124
137
  """Enter the context manager and set this context as the current context."""
125
138
  parent_context = _current_context.get(None)
@@ -188,7 +201,8 @@ class Context:
188
201
  Returns:
189
202
  The resource content as either text or bytes
190
203
  """
191
- assert self.fastmcp is not None, "Context is not available outside of a request"
204
+ if self.fastmcp is None:
205
+ raise ValueError("Context is not available outside of a request")
192
206
  return await self.fastmcp._mcp_read_resource(uri)
193
207
 
194
208
  async def log(
@@ -376,13 +390,50 @@ class Context:
376
390
  for m in messages
377
391
  ]
378
392
 
393
+ should_fallback = (
394
+ self.fastmcp.sampling_handler_behavior == "fallback"
395
+ and not self.session.check_client_capability(
396
+ capability=ClientCapabilities(sampling=SamplingCapability())
397
+ )
398
+ )
399
+
400
+ if self.fastmcp.sampling_handler_behavior == "always" or should_fallback:
401
+ if self.fastmcp.sampling_handler is None:
402
+ raise ValueError("Client does not support sampling")
403
+
404
+ create_message_result = self.fastmcp.sampling_handler(
405
+ sampling_messages,
406
+ SamplingParams(
407
+ systemPrompt=system_prompt,
408
+ messages=sampling_messages,
409
+ temperature=temperature,
410
+ maxTokens=max_tokens,
411
+ modelPreferences=_parse_model_preferences(model_preferences),
412
+ ),
413
+ self.request_context,
414
+ )
415
+
416
+ if inspect.isawaitable(create_message_result):
417
+ create_message_result = await create_message_result
418
+
419
+ if isinstance(create_message_result, str):
420
+ return TextContent(text=create_message_result, type="text")
421
+
422
+ if isinstance(create_message_result, CreateMessageResult):
423
+ return create_message_result.content
424
+
425
+ else:
426
+ raise ValueError(
427
+ f"Unexpected sampling handler result: {create_message_result}"
428
+ )
429
+
379
430
  result: CreateMessageResult = await self.session.create_message(
380
431
  messages=sampling_messages,
381
432
  system_prompt=system_prompt,
382
433
  include_context=include_context,
383
434
  temperature=temperature,
384
435
  max_tokens=max_tokens,
385
- model_preferences=self._parse_model_preferences(model_preferences),
436
+ model_preferences=_parse_model_preferences(model_preferences),
386
437
  related_request_id=self.request_id,
387
438
  )
388
439
 
@@ -497,7 +548,7 @@ class Context:
497
548
  if isinstance(validated_data, ScalarElicitationType):
498
549
  return AcceptedElicitation[T](data=validated_data.value)
499
550
  else:
500
- return AcceptedElicitation[T](data=validated_data)
551
+ return AcceptedElicitation[T](data=cast(T, validated_data))
501
552
  elif result.content:
502
553
  raise ValueError(
503
554
  "Elicitation expected an empty response, but received: "
@@ -582,44 +633,43 @@ class Context:
582
633
  # Don't let notification failures break the request
583
634
  pass
584
635
 
585
- def _parse_model_preferences(
586
- self, model_preferences: ModelPreferences | str | list[str] | None
587
- ) -> ModelPreferences | None:
588
- """
589
- Validates and converts user input for model_preferences into a ModelPreferences object.
590
636
 
591
- Args:
592
- model_preferences (ModelPreferences | str | list[str] | None):
593
- The model preferences to use. Accepts:
594
- - ModelPreferences (returns as-is)
595
- - str (single model hint)
596
- - list[str] (multiple model hints)
597
- - None (no preferences)
637
+ def _parse_model_preferences(
638
+ model_preferences: ModelPreferences | str | list[str] | None,
639
+ ) -> ModelPreferences | None:
640
+ """
641
+ Validates and converts user input for model_preferences into a ModelPreferences object.
598
642
 
599
- Returns:
600
- ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
643
+ Args:
644
+ model_preferences (ModelPreferences | str | list[str] | None):
645
+ The model preferences to use. Accepts:
646
+ - ModelPreferences (returns as-is)
647
+ - str (single model hint)
648
+ - list[str] (multiple model hints)
649
+ - None (no preferences)
601
650
 
602
- Raises:
603
- ValueError: If the input is not a supported type or contains invalid values.
604
- """
605
- if model_preferences is None:
606
- return None
607
- elif isinstance(model_preferences, ModelPreferences):
608
- return model_preferences
609
- elif isinstance(model_preferences, str):
610
- # Single model hint
611
- return ModelPreferences(hints=[ModelHint(name=model_preferences)])
612
- elif isinstance(model_preferences, list):
613
- # List of model hints (strings)
614
- if not all(isinstance(h, str) for h in model_preferences):
615
- raise ValueError(
616
- "All elements of model_preferences list must be"
617
- " strings (model name hints)."
618
- )
619
- return ModelPreferences(
620
- hints=[ModelHint(name=h) for h in model_preferences]
621
- )
622
- else:
651
+ Returns:
652
+ ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
653
+
654
+ Raises:
655
+ ValueError: If the input is not a supported type or contains invalid values.
656
+ """
657
+ if model_preferences is None:
658
+ return None
659
+ elif isinstance(model_preferences, ModelPreferences):
660
+ return model_preferences
661
+ elif isinstance(model_preferences, str):
662
+ # Single model hint
663
+ return ModelPreferences(hints=[ModelHint(name=model_preferences)])
664
+ elif isinstance(model_preferences, list):
665
+ # List of model hints (strings)
666
+ if not all(isinstance(h, str) for h in model_preferences):
623
667
  raise ValueError(
624
- "model_preferences must be one of: ModelPreferences, str, list[str], or None."
668
+ "All elements of model_preferences list must be"
669
+ " strings (model name hints)."
625
670
  )
671
+ return ModelPreferences(hints=[ModelHint(name=h) for h in model_preferences])
672
+ else:
673
+ raise ValueError(
674
+ "model_preferences must be one of: ModelPreferences, str, list[str], or None."
675
+ )
@@ -2,10 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, ParamSpec, TypeVar
4
4
 
5
- from mcp.server.auth.middleware.auth_context import get_access_token
6
- from mcp.server.auth.provider import AccessToken
5
+ from mcp.server.auth.middleware.auth_context import (
6
+ get_access_token as _sdk_get_access_token,
7
+ )
7
8
  from starlette.requests import Request
8
9
 
10
+ from fastmcp.server.auth import AccessToken
11
+
9
12
  if TYPE_CHECKING:
10
13
  from fastmcp.server.context import Context
11
14
 
@@ -94,3 +97,30 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
94
97
  return headers
95
98
  except RuntimeError:
96
99
  return {}
100
+
101
+
102
+ # --- Access Token ---
103
+
104
+
105
+ def get_access_token() -> AccessToken | None:
106
+ """
107
+ Get the FastMCP access token from the current context.
108
+
109
+ Returns:
110
+ The access token if an authenticated user is available, None otherwise.
111
+ """
112
+ #
113
+ obj = _sdk_get_access_token()
114
+ if obj is None or isinstance(obj, AccessToken):
115
+ return obj
116
+
117
+ # If the object is not a FastMCP AccessToken, convert it to one if the fields are compatible
118
+ # This is a workaround for the case where the SDK returns a different type
119
+ # If it fails, it will raise a TypeError
120
+ try:
121
+ return AccessToken(**obj.model_dump())
122
+ except Exception as e:
123
+ raise TypeError(
124
+ f"Expected fastmcp.server.auth.auth.AccessToken, got {type(obj).__name__}. "
125
+ "Ensure the SDK is using the correct AccessToken type."
126
+ ) from e
@@ -8,6 +8,8 @@ from mcp.server.elicitation import (
8
8
  DeclinedElicitation,
9
9
  )
10
10
  from pydantic import BaseModel
11
+ from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
12
+ from pydantic_core import core_schema
11
13
 
12
14
  from fastmcp.utilities.json_schema import compress_schema
13
15
  from fastmcp.utilities.logging import get_logger
@@ -26,6 +28,60 @@ logger = get_logger(__name__)
26
28
  T = TypeVar("T")
27
29
 
28
30
 
31
+ class ElicitationJsonSchema(GenerateJsonSchema):
32
+ """Custom JSON schema generator for MCP elicitation that always inlines enums.
33
+
34
+ MCP elicitation requires inline enum schemas without $ref/$defs references.
35
+ This generator ensures enums are always generated inline for compatibility.
36
+ Optionally adds enumNames for better UI display when available.
37
+ """
38
+
39
+ def generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue:
40
+ """Override to prevent ref generation for enums."""
41
+ # For enum schemas, bypass the ref mechanism entirely
42
+ if schema["type"] == "enum":
43
+ # Directly call our custom enum_schema without going through handler
44
+ # This prevents the ref/defs mechanism from being invoked
45
+ return self.enum_schema(schema)
46
+ # For all other types, use the default implementation
47
+ return super().generate_inner(schema)
48
+
49
+ def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
50
+ """Generate inline enum schema with optional enumNames for better UI.
51
+
52
+ If enum members have a _display_name_ attribute or custom __str__,
53
+ we'll include enumNames for better UI representation.
54
+ """
55
+ # Get the base schema from parent
56
+ result = super().enum_schema(schema)
57
+
58
+ # Try to add enumNames if the enum has display-friendly names
59
+ enum_cls = schema.get("cls")
60
+ if enum_cls:
61
+ members = schema.get("members", [])
62
+ enum_names = []
63
+ has_custom_names = False
64
+
65
+ for member in members:
66
+ # Check if member has a custom display name attribute
67
+ if hasattr(member, "_display_name_"):
68
+ enum_names.append(member._display_name_)
69
+ has_custom_names = True
70
+ # Or use the member name with better formatting
71
+ else:
72
+ # Convert SNAKE_CASE to Title Case for display
73
+ display_name = member.name.replace("_", " ").title()
74
+ enum_names.append(display_name)
75
+ if display_name != member.value:
76
+ has_custom_names = True
77
+
78
+ # Only add enumNames if they differ from the values
79
+ if has_custom_names:
80
+ result["enumNames"] = enum_names
81
+
82
+ return result
83
+
84
+
29
85
  # we can't use the low-level AcceptedElicitation because it only works with BaseModels
30
86
  class AcceptedElicitation(BaseModel, Generic[T]):
31
87
  """Result when user accepts the elicitation."""
@@ -46,7 +102,10 @@ def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
46
102
  response_type: The type of the response
47
103
  """
48
104
 
49
- schema = get_cached_typeadapter(response_type).json_schema()
105
+ # Use custom schema generator that inlines enums for MCP compatibility
106
+ schema = get_cached_typeadapter(response_type).json_schema(
107
+ schema_generator=ElicitationJsonSchema
108
+ )
50
109
  schema = compress_schema(schema)
51
110
 
52
111
  # Validate the schema to ensure it follows MCP elicitation requirements
fastmcp/server/http.py CHANGED
@@ -23,7 +23,7 @@ from starlette.responses import Response
23
23
  from starlette.routing import BaseRoute, Mount, Route
24
24
  from starlette.types import Lifespan, Receive, Scope, Send
25
25
 
26
- from fastmcp.server.auth.auth import AuthProvider
26
+ from fastmcp.server.auth import AuthProvider
27
27
  from fastmcp.utilities.logging import get_logger
28
28
 
29
29
  if TYPE_CHECKING:
@@ -32,7 +32,39 @@ if TYPE_CHECKING:
32
32
  logger = get_logger(__name__)
33
33
 
34
34
 
35
- _current_http_request: ContextVar[Request | None] = ContextVar(
35
+ class StreamableHTTPASGIApp:
36
+ """ASGI application wrapper for Streamable HTTP server transport."""
37
+
38
+ def __init__(self, session_manager):
39
+ self.session_manager = session_manager
40
+
41
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
42
+ try:
43
+ await self.session_manager.handle_request(scope, receive, send)
44
+ except RuntimeError as e:
45
+ if str(e) == "Task group is not initialized. Make sure to use run().":
46
+ logger.error(
47
+ f"Original RuntimeError from mcp library: {e}", exc_info=True
48
+ )
49
+ new_error_message = (
50
+ "FastMCP's StreamableHTTPSessionManager task group was not initialized. "
51
+ "This commonly occurs when the FastMCP application's lifespan is not "
52
+ "passed to the parent ASGI application (e.g., FastAPI or Starlette). "
53
+ "Please ensure you are setting `lifespan=mcp_app.lifespan` in your "
54
+ "parent app's constructor, where `mcp_app` is the application instance "
55
+ "returned by `fastmcp_instance.http_app()`. \\n"
56
+ "For more details, see the FastMCP ASGI integration documentation: "
57
+ "https://gofastmcp.com/deployment/asgi"
58
+ )
59
+ # Raise a new RuntimeError that includes the original error's message
60
+ # for full context, but leads with the more helpful guidance.
61
+ raise RuntimeError(f"{new_error_message}\\nOriginal error: {e}") from e
62
+ else:
63
+ # Re-raise other RuntimeErrors if they don't match the specific message
64
+ raise
65
+
66
+
67
+ _current_http_request: ContextVar[Request | None] = ContextVar( # type: ignore[assignment]
36
68
  "http_request",
37
69
  default=None,
38
70
  )
@@ -40,7 +72,7 @@ _current_http_request: ContextVar[Request | None] = ContextVar(
40
72
 
41
73
  class StarletteWithLifespan(Starlette):
42
74
  @property
43
- def lifespan(self) -> Lifespan:
75
+ def lifespan(self) -> Lifespan[Starlette]:
44
76
  return self.router.lifespan_context
45
77
 
46
78
 
@@ -197,7 +229,7 @@ def create_sse_app(
197
229
  # Add custom routes with lowest precedence
198
230
  if routes:
199
231
  server_routes.extend(routes)
200
- server_routes.extend(server._additional_http_routes)
232
+ server_routes.extend(server._get_additional_http_routes())
201
233
 
202
234
  # Add middleware
203
235
  if middleware:
@@ -254,33 +286,8 @@ def create_streamable_http_app(
254
286
  stateless=stateless_http,
255
287
  )
256
288
 
257
- # Create the ASGI handler
258
- async def handle_streamable_http(
259
- scope: Scope, receive: Receive, send: Send
260
- ) -> None:
261
- try:
262
- await session_manager.handle_request(scope, receive, send)
263
- except RuntimeError as e:
264
- if str(e) == "Task group is not initialized. Make sure to use run().":
265
- logger.error(
266
- f"Original RuntimeError from mcp library: {e}", exc_info=True
267
- )
268
- new_error_message = (
269
- "FastMCP's StreamableHTTPSessionManager task group was not initialized. "
270
- "This commonly occurs when the FastMCP application's lifespan is not "
271
- "passed to the parent ASGI application (e.g., FastAPI or Starlette). "
272
- "Please ensure you are setting `lifespan=mcp_app.lifespan` in your "
273
- "parent app's constructor, where `mcp_app` is the application instance "
274
- "returned by `fastmcp_instance.http_app()`. \\n"
275
- "For more details, see the FastMCP ASGI integration documentation: "
276
- "https://gofastmcp.com/deployment/asgi"
277
- )
278
- # Raise a new RuntimeError that includes the original error's message
279
- # for full context, but leads with the more helpful guidance.
280
- raise RuntimeError(f"{new_error_message}\\nOriginal error: {e}") from e
281
- else:
282
- # Re-raise other RuntimeErrors if they don't match the specific message
283
- raise
289
+ # Create the ASGI app wrapper
290
+ streamable_http_app = StreamableHTTPASGIApp(session_manager)
284
291
 
285
292
  # Add StreamableHTTP routes with or without auth
286
293
  if auth:
@@ -305,26 +312,26 @@ def create_streamable_http_app(
305
312
 
306
313
  # Auth is enabled, wrap endpoint with RequireAuthMiddleware
307
314
  server_routes.append(
308
- Mount(
315
+ Route(
309
316
  streamable_http_path,
310
- app=RequireAuthMiddleware(
311
- handle_streamable_http, required_scopes, resource_metadata_url
317
+ endpoint=RequireAuthMiddleware(
318
+ streamable_http_app, required_scopes, resource_metadata_url
312
319
  ),
313
320
  )
314
321
  )
315
322
  else:
316
323
  # No auth required
317
324
  server_routes.append(
318
- Mount(
325
+ Route(
319
326
  streamable_http_path,
320
- app=handle_streamable_http,
327
+ endpoint=streamable_http_app,
321
328
  )
322
329
  )
323
330
 
324
331
  # Add custom routes with lowest precedence
325
332
  if routes:
326
333
  server_routes.extend(routes)
327
- server_routes.extend(server._additional_http_routes)
334
+ server_routes.extend(server._get_additional_http_routes())
328
335
 
329
336
  # Add middleware
330
337
  if middleware: