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
@@ -7,7 +7,7 @@ import mcp.types
7
7
  from mcp import ClientSession
8
8
  from mcp.client.session import ElicitationFnT
9
9
  from mcp.shared.context import LifespanContextT, RequestContext
10
- from mcp.types import ElicitRequestParams
10
+ from mcp.types import ElicitRequestFormParams, ElicitRequestParams
11
11
  from mcp.types import ElicitResult as MCPElicitResult
12
12
  from pydantic_core import to_jsonable_python
13
13
  from typing_extensions import TypeVar
@@ -26,7 +26,8 @@ class ElicitResult(MCPElicitResult, Generic[T]):
26
26
  ElicitationHandler: TypeAlias = Callable[
27
27
  [
28
28
  str, # message
29
- type[T], # a class for creating a structured response
29
+ type[T]
30
+ | None, # a class for creating a structured response (None for URL elicitation)
30
31
  ElicitRequestParams,
31
32
  RequestContext[ClientSession, LifespanContextT],
32
33
  ],
@@ -42,10 +43,15 @@ def create_elicitation_callback(
42
43
  params: ElicitRequestParams,
43
44
  ) -> MCPElicitResult | mcp.types.ErrorData:
44
45
  try:
45
- if params.requestedSchema == {"type": "object", "properties": {}}:
46
- response_type = None
46
+ # requestedSchema only exists on ElicitRequestFormParams, not ElicitRequestURLParams
47
+ if isinstance(params, ElicitRequestFormParams):
48
+ if params.requestedSchema == {"type": "object", "properties": {}}:
49
+ response_type = None
50
+ else:
51
+ response_type = json_schema_to_type(params.requestedSchema)
47
52
  else:
48
- response_type = json_schema_to_type(params.requestedSchema)
53
+ # URL-based elicitation doesn't have a schema
54
+ response_type = None
49
55
 
50
56
  result = await elicitation_handler(
51
57
  params.message, response_type, params, context
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:
@@ -35,16 +35,18 @@ class MessageHandler:
35
35
  # requests
36
36
  case RequestResponder():
37
37
  # handle all requests
38
- await self.on_request(message)
38
+ # TODO(ty): remove when ty supports match statement narrowing
39
+ await self.on_request(message) # type: ignore[arg-type]
39
40
 
40
41
  # handle specific requests
41
- match message.request.root:
42
+ # TODO(ty): remove type ignores when ty supports match statement narrowing
43
+ match message.request.root: # type: ignore[union-attr]
42
44
  case mcp.types.PingRequest():
43
- await self.on_ping(message.request.root)
45
+ await self.on_ping(message.request.root) # type: ignore[union-attr]
44
46
  case mcp.types.ListRootsRequest():
45
- await self.on_list_roots(message.request.root)
47
+ await self.on_list_roots(message.request.root) # type: ignore[union-attr]
46
48
  case mcp.types.CreateMessageRequest():
47
- await self.on_create_message(message.request.root)
49
+ await self.on_create_message(message.request.root) # type: ignore[union-attr]
48
50
 
49
51
  # notifications
50
52
  case mcp.types.ServerNotification():
@@ -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,43 @@ 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(
50
+ f"Connected to: {server_url}", centered=True, monospace=True
51
+ )
52
52
  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>
53
+ detail_info = create_info_box(
54
+ message, is_error=True, centered=True, monospace=True
55
+ )
56
+
57
+ # Build the page content
58
+ content = f"""
166
59
  <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>
60
+ {create_logo()}
61
+ {create_status_message(status_title, is_success=is_success)}
172
62
  {detail_info}
173
63
  <div class="close-instruction">
174
64
  You can safely close this tab now.
175
65
  </div>
176
66
  </div>
177
- </body>
178
- </html>
179
67
  """
180
68
 
69
+ # Additional styles needed for this page
70
+ additional_styles = STATUS_MESSAGE_STYLES + INFO_BOX_STYLES + HELPER_TEXT_STYLES
71
+
72
+ return create_page(
73
+ content=content,
74
+ title=title,
75
+ additional_styles=additional_styles,
76
+ )
77
+
181
78
 
182
79
  @dataclass
183
80
  class CallbackResponse:
@@ -194,11 +91,21 @@ class CallbackResponse:
194
91
  return {k: v for k, v in self.__dict__.items() if v is not None}
195
92
 
196
93
 
94
+ @dataclass
95
+ class OAuthCallbackResult:
96
+ """Container for OAuth callback results, used with anyio.Event for async coordination."""
97
+
98
+ code: str | None = None
99
+ state: str | None = None
100
+ error: Exception | None = None
101
+
102
+
197
103
  def create_oauth_callback_server(
198
104
  port: int,
199
105
  callback_path: str = "/callback",
200
106
  server_url: str | None = None,
201
- response_future: asyncio.Future | None = None,
107
+ result_container: OAuthCallbackResult | None = None,
108
+ result_ready: anyio.Event | None = None,
202
109
  ) -> Server:
203
110
  """
204
111
  Create an OAuth callback server.
@@ -207,7 +114,8 @@ def create_oauth_callback_server(
207
114
  port: The port to run the server on
208
115
  callback_path: The path to listen for OAuth redirects on
209
116
  server_url: Optional server URL to display in success messages
210
- response_future: Optional future to resolve when OAuth callback is received
117
+ result_container: Optional container to store callback results
118
+ result_ready: Optional event to signal when callback is received
211
119
 
212
120
  Returns:
213
121
  Configured uvicorn Server instance (not yet running)
@@ -221,32 +129,36 @@ def create_oauth_callback_server(
221
129
  if callback_response.error:
222
130
  error_desc = callback_response.error_description or "Unknown error"
223
131
 
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
- )
132
+ # Create user-friendly error messages
133
+ if callback_response.error == "access_denied":
134
+ user_message = "Access was denied by the authorization server."
135
+ else:
136
+ user_message = f"Authorization failed: {error_desc}"
137
+
138
+ # Store error and signal completion if result tracking provided
139
+ if result_container is not None and result_ready is not None:
140
+ result_container.error = RuntimeError(user_message)
141
+ result_ready.set()
231
142
 
232
- return HTMLResponse(
143
+ return create_secure_html_response(
233
144
  create_callback_html(
234
- f"FastMCP OAuth Error: {callback_response.error}<br>{error_desc}",
145
+ user_message,
235
146
  is_success=False,
236
147
  ),
237
148
  status_code=400,
238
149
  )
239
150
 
240
151
  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
- )
152
+ user_message = "No authorization code was received from the server."
246
153
 
247
- return HTMLResponse(
154
+ # Store error and signal completion if result tracking provided
155
+ if result_container is not None and result_ready is not None:
156
+ result_container.error = RuntimeError(user_message)
157
+ result_ready.set()
158
+
159
+ return create_secure_html_response(
248
160
  create_callback_html(
249
- "FastMCP OAuth Error: No authorization code received",
161
+ user_message,
250
162
  is_success=False,
251
163
  ),
252
164
  status_code=400,
@@ -254,29 +166,30 @@ def create_oauth_callback_server(
254
166
 
255
167
  # Check for missing state parameter (indicates OAuth flow issue)
256
168
  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(
169
+ user_message = (
170
+ "The OAuth server did not return the expected state parameter."
171
+ )
172
+
173
+ # Store error and signal completion if result tracking provided
174
+ if result_container is not None and result_ready is not None:
175
+ result_container.error = RuntimeError(user_message)
176
+ result_ready.set()
177
+
178
+ return create_secure_html_response(
266
179
  create_callback_html(
267
- "FastMCP OAuth Error: Authentication failed<br>The OAuth server did not return the expected state parameter",
180
+ user_message,
268
181
  is_success=False,
269
182
  ),
270
183
  status_code=400,
271
184
  )
272
185
 
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
- )
186
+ # Success case - store result and signal completion if result tracking provided
187
+ if result_container is not None and result_ready is not None:
188
+ result_container.code = callback_response.code
189
+ result_container.state = callback_response.state
190
+ result_ready.set()
278
191
 
279
- return HTMLResponse(
192
+ return create_secure_html_response(
280
193
  create_callback_html("", is_success=True, server_url=server_url)
281
194
  )
282
195
 
@@ -289,6 +202,7 @@ def create_oauth_callback_server(
289
202
  port=port,
290
203
  lifespan="off",
291
204
  log_level="warning",
205
+ ws="websockets-sansio",
292
206
  )
293
207
  )
294
208
 
fastmcp/client/roots.py CHANGED
@@ -34,7 +34,8 @@ def create_roots_callback(
34
34
  handler: RootsList | RootsHandler,
35
35
  ) -> ListRootsFnT:
36
36
  if isinstance(handler, list):
37
- return _create_roots_callback_from_roots(handler)
37
+ # TODO(ty): remove when ty supports isinstance union narrowing
38
+ return _create_roots_callback_from_roots(handler) # type: ignore[arg-type]
38
39
  elif inspect.isfunction(handler):
39
40
  return _create_roots_callback_from_fn(handler)
40
41
  else:
@@ -11,7 +11,7 @@ from mcp.types import SamplingMessage
11
11
 
12
12
  from fastmcp.server.sampling.handler import ServerSamplingHandler
13
13
 
14
- __all__ = ["SamplingMessage", "SamplingParams", "SamplingHandler"]
14
+ __all__ = ["SamplingHandler", "SamplingMessage", "SamplingParams"]
15
15
 
16
16
 
17
17
  ClientSamplingHandler: TypeAlias = Callable[