fastmcp 2.11.2__py3-none-any.whl → 2.12.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.0rc1.dist-info}/METADATA +3 -2
  73. fastmcp-2.12.0rc1.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.0rc1.dist-info}/WHEEL +0 -0
  76. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/entry_points.txt +0 -0
  77. {fastmcp-2.11.2.dist-info → fastmcp-2.12.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import json
5
5
  import webbrowser
6
+ from asyncio import Future
7
+ from datetime import datetime, timedelta, timezone
6
8
  from pathlib import Path
7
9
  from typing import Any, Literal
8
10
  from urllib.parse import urlparse
@@ -17,7 +19,8 @@ from mcp.shared.auth import (
17
19
  from mcp.shared.auth import (
18
20
  OAuthToken as OAuthToken,
19
21
  )
20
- from pydantic import AnyHttpUrl, ValidationError
22
+ from pydantic import AnyHttpUrl, BaseModel, TypeAdapter, ValidationError
23
+ from uvicorn.server import Server
21
24
 
22
25
  from fastmcp import settings as fastmcp_global_settings
23
26
  from fastmcp.client.oauth_callback import (
@@ -31,6 +34,17 @@ __all__ = ["OAuth"]
31
34
  logger = get_logger(__name__)
32
35
 
33
36
 
37
+ class StoredToken(BaseModel):
38
+ """Token storage format with absolute expiry time."""
39
+
40
+ token_payload: OAuthToken
41
+ expires_at: datetime | None
42
+
43
+
44
+ # Create TypeAdapter at module level for efficient parsing
45
+ stored_token_adapter = TypeAdapter(StoredToken)
46
+
47
+
34
48
  def default_cache_dir() -> Path:
35
49
  return fastmcp_global_settings.home / "oauth-mcp-client-cache"
36
50
 
@@ -75,13 +89,28 @@ class FileTokenStorage(TokenStorage):
75
89
  path = self._get_file_path("tokens")
76
90
 
77
91
  try:
78
- tokens = OAuthToken.model_validate_json(path.read_text())
79
- # now = datetime.datetime.now(datetime.timezone.utc)
80
- # if tokens.expires_at is not None and tokens.expires_at <= now:
81
- # logger.debug(f"Token expired for {self.get_base_url(self.server_url)}")
82
- # return None
83
- return tokens
84
- except (FileNotFoundError, json.JSONDecodeError, ValidationError) as e:
92
+ # Parse JSON and validate as StoredToken
93
+ stored = stored_token_adapter.validate_json(path.read_text())
94
+
95
+ # Check if token is expired
96
+ if stored.expires_at is not None:
97
+ now = datetime.now(timezone.utc)
98
+ if now >= stored.expires_at:
99
+ logger.debug(
100
+ f"Token expired for {self.get_base_url(self.server_url)}"
101
+ )
102
+ return None
103
+
104
+ # Recalculate expires_in to be correct relative to now
105
+ if stored.token_payload.expires_in is not None:
106
+ remaining = stored.expires_at - now
107
+ stored.token_payload.expires_in = max(
108
+ 0, int(remaining.total_seconds())
109
+ )
110
+
111
+ return stored.token_payload
112
+
113
+ except (FileNotFoundError, ValidationError) as e:
85
114
  logger.debug(
86
115
  f"Could not load tokens for {self.get_base_url(self.server_url)}: {e}"
87
116
  )
@@ -90,7 +119,18 @@ class FileTokenStorage(TokenStorage):
90
119
  async def set_tokens(self, tokens: OAuthToken) -> None:
91
120
  """Save tokens to file storage."""
92
121
  path = self._get_file_path("tokens")
93
- path.write_text(tokens.model_dump_json(indent=2))
122
+
123
+ # Calculate absolute expiry time if expires_in is present
124
+ expires_at = None
125
+ if tokens.expires_in is not None:
126
+ expires_at = datetime.now(timezone.utc) + timedelta(
127
+ seconds=tokens.expires_in
128
+ )
129
+
130
+ # Create StoredToken and save using Pydantic serialization
131
+ stored = StoredToken(token_payload=tokens, expires_at=expires_at)
132
+
133
+ path.write_text(stored.model_dump_json(indent=2))
94
134
  logger.debug(f"Saved tokens for {self.get_base_url(self.server_url)}")
95
135
 
96
136
  async def get_client_info(self) -> OAuthClientInformationFull | None:
@@ -215,8 +255,13 @@ class OAuth(OAuthClientProvider):
215
255
  self.redirect_port = callback_port or find_available_port()
216
256
  redirect_uri = f"http://localhost:{self.redirect_port}/callback"
217
257
 
258
+ scopes_str: str
218
259
  if isinstance(scopes, list):
219
- scopes = " ".join(scopes)
260
+ scopes_str = " ".join(scopes)
261
+ elif scopes is not None:
262
+ scopes_str = str(scopes)
263
+ else:
264
+ scopes_str = ""
220
265
 
221
266
  client_metadata = OAuthClientMetadata(
222
267
  client_name=client_name,
@@ -224,7 +269,7 @@ class OAuth(OAuthClientProvider):
224
269
  grant_types=["authorization_code", "refresh_token"],
225
270
  response_types=["code"],
226
271
  # token_endpoint_auth_method="client_secret_post",
227
- scope=scopes,
272
+ scope=scopes_str,
228
273
  **(additional_client_metadata or {}),
229
274
  )
230
275
 
@@ -245,6 +290,15 @@ class OAuth(OAuthClientProvider):
245
290
  callback_handler=self.callback_handler,
246
291
  )
247
292
 
293
+ async def _initialize(self) -> None:
294
+ """Load stored tokens and client info, properly setting token expiry."""
295
+ # Call parent's _initialize to load tokens and client info
296
+ await super()._initialize()
297
+
298
+ # If tokens were loaded and have expires_in, update the context's token_expiry_time
299
+ if self.context.current_tokens and self.context.current_tokens.expires_in:
300
+ self.context.update_token_expiry(self.context.current_tokens)
301
+
248
302
  async def redirect_handler(self, authorization_url: str) -> None:
249
303
  """Open browser for authorization."""
250
304
  logger.info(f"OAuth authorization URL: {authorization_url}")
@@ -253,10 +307,10 @@ class OAuth(OAuthClientProvider):
253
307
  async def callback_handler(self) -> tuple[str, str | None]:
254
308
  """Handle OAuth callback and return (auth_code, state)."""
255
309
  # Create a future to capture the OAuth response
256
- response_future = asyncio.get_running_loop().create_future()
310
+ response_future: Future[Any] = asyncio.get_running_loop().create_future()
257
311
 
258
312
  # Create server with the future
259
- server = create_oauth_callback_server(
313
+ server: Server = create_oauth_callback_server(
260
314
  port=self.redirect_port,
261
315
  server_url=self.server_base_url,
262
316
  response_future=response_future,
@@ -280,3 +334,5 @@ class OAuth(OAuthClientProvider):
280
334
  server.should_exit = True
281
335
  await asyncio.sleep(0.1) # Allow server to shutdown gracefully
282
336
  tg.cancel_scope.cancel()
337
+
338
+ raise RuntimeError("OAuth callback handler could not be started")
fastmcp/client/client.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import copy
5
5
  import datetime
6
+ import secrets
6
7
  from contextlib import AsyncExitStack, asynccontextmanager
7
8
  from dataclasses import dataclass, field
8
9
  from pathlib import Path
@@ -30,7 +31,11 @@ from fastmcp.client.roots import (
30
31
  RootsList,
31
32
  create_roots_callback,
32
33
  )
33
- from fastmcp.client.sampling import SamplingHandler, create_sampling_callback
34
+ from fastmcp.client.sampling import (
35
+ ClientSamplingHandler,
36
+ SamplingHandler,
37
+ create_sampling_callback,
38
+ )
34
39
  from fastmcp.exceptions import ToolError
35
40
  from fastmcp.mcp_config import MCPConfig
36
41
  from fastmcp.server import FastMCP
@@ -49,6 +54,7 @@ from .transports import (
49
54
  PythonStdioTransport,
50
55
  SessionKwargs,
51
56
  SSETransport,
57
+ StdioTransport,
52
58
  StreamableHttpTransport,
53
59
  infer_transport,
54
60
  )
@@ -60,6 +66,7 @@ __all__ = [
60
66
  "RootsList",
61
67
  "LogHandler",
62
68
  "MessageHandler",
69
+ "ClientSamplingHandler",
63
70
  "SamplingHandler",
64
71
  "ElicitationHandler",
65
72
  "ProgressHandler",
@@ -207,8 +214,9 @@ class Client(Generic[ClientTransportT]):
207
214
  | dict[str, Any]
208
215
  | str
209
216
  ),
217
+ name: str | None = None,
210
218
  roots: RootsList | RootsHandler | None = None,
211
- sampling_handler: SamplingHandler | None = None,
219
+ sampling_handler: ClientSamplingHandler | None = None,
212
220
  elicitation_handler: ElicitationHandler | None = None,
213
221
  log_handler: LogHandler | None = None,
214
222
  message_handler: MessageHandlerT | MessageHandler | None = None,
@@ -218,6 +226,8 @@ class Client(Generic[ClientTransportT]):
218
226
  client_info: mcp.types.Implementation | None = None,
219
227
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
220
228
  ) -> None:
229
+ self.name = name or self.generate_name()
230
+
221
231
  self.transport = cast(ClientTransportT, infer_transport(transport))
222
232
  if auth is not None:
223
233
  self.transport._set_auth(auth)
@@ -231,7 +241,7 @@ class Client(Generic[ClientTransportT]):
231
241
  self._progress_handler = progress_handler
232
242
 
233
243
  if isinstance(timeout, int | float):
234
- timeout = datetime.timedelta(seconds=timeout)
244
+ timeout = datetime.timedelta(seconds=float(timeout))
235
245
 
236
246
  # handle init handshake timeout
237
247
  if init_timeout is None:
@@ -292,7 +302,7 @@ class Client(Generic[ClientTransportT]):
292
302
  """Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
293
303
  self._session_kwargs["list_roots_callback"] = create_roots_callback(roots)
294
304
 
295
- def set_sampling_callback(self, sampling_callback: SamplingHandler) -> None:
305
+ def set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None:
296
306
  """Set the sampling callback for the client."""
297
307
  self._session_kwargs["sampling_callback"] = create_sampling_callback(
298
308
  sampling_callback
@@ -328,11 +338,13 @@ class Client(Generic[ClientTransportT]):
328
338
  await fresh_client.call_tool("some_tool", {})
329
339
  ```
330
340
  """
331
-
332
341
  new_client = copy.copy(self)
333
342
 
334
- # Reset session state to fresh state
335
- new_client._session_state = ClientSessionState()
343
+ if not isinstance(self.transport, StdioTransport):
344
+ # Reset session state to fresh state
345
+ new_client._session_state = ClientSessionState()
346
+
347
+ new_client.name += f":{secrets.token_hex(2)}"
336
348
 
337
349
  return new_client
338
350
 
@@ -383,6 +395,7 @@ class Client(Generic[ClientTransportT]):
383
395
  self._session_state.session_task is None
384
396
  or self._session_state.session_task.done()
385
397
  )
398
+
386
399
  if need_to_start:
387
400
  if self._session_state.nesting_counter != 0:
388
401
  raise RuntimeError(
@@ -408,6 +421,7 @@ class Client(Generic[ClientTransportT]):
408
421
  ) from exception
409
422
 
410
423
  self._session_state.nesting_counter += 1
424
+
411
425
  return self
412
426
 
413
427
  async def _disconnect(self, force: bool = False):
@@ -533,6 +547,8 @@ class Client(Generic[ClientTransportT]):
533
547
  Raises:
534
548
  RuntimeError: If called while the client is not connected.
535
549
  """
550
+ logger.debug(f"[{self.name}] called list_resources")
551
+
536
552
  result = await self.session.list_resources()
537
553
  return result
538
554
 
@@ -560,6 +576,8 @@ class Client(Generic[ClientTransportT]):
560
576
  Raises:
561
577
  RuntimeError: If called while the client is not connected.
562
578
  """
579
+ logger.debug(f"[{self.name}] called list_resource_templates")
580
+
563
581
  result = await self.session.list_resource_templates()
564
582
  return result
565
583
 
@@ -592,6 +610,8 @@ class Client(Generic[ClientTransportT]):
592
610
  Raises:
593
611
  RuntimeError: If called while the client is not connected.
594
612
  """
613
+ logger.debug(f"[{self.name}] called read_resource: {uri}")
614
+
595
615
  if isinstance(uri, str):
596
616
  uri = AnyUrl(uri) # Ensure AnyUrl
597
617
  result = await self.session.read_resource(uri)
@@ -646,6 +666,8 @@ class Client(Generic[ClientTransportT]):
646
666
  Raises:
647
667
  RuntimeError: If called while the client is not connected.
648
668
  """
669
+ logger.debug(f"[{self.name}] called list_prompts")
670
+
649
671
  result = await self.session.list_prompts()
650
672
  return result
651
673
 
@@ -678,6 +700,8 @@ class Client(Generic[ClientTransportT]):
678
700
  Raises:
679
701
  RuntimeError: If called while the client is not connected.
680
702
  """
703
+ logger.debug(f"[{self.name}] called get_prompt: {name}")
704
+
681
705
  # Serialize arguments for MCP protocol - convert non-string values to JSON
682
706
  serialized_arguments: dict[str, str] | None = None
683
707
  if arguments:
@@ -735,6 +759,8 @@ class Client(Generic[ClientTransportT]):
735
759
  Raises:
736
760
  RuntimeError: If called while the client is not connected.
737
761
  """
762
+ logger.debug(f"[{self.name}] called complete: {ref}")
763
+
738
764
  result = await self.session.complete(ref=ref, argument=argument)
739
765
  return result
740
766
 
@@ -770,6 +796,8 @@ class Client(Generic[ClientTransportT]):
770
796
  Raises:
771
797
  RuntimeError: If called while the client is not connected.
772
798
  """
799
+ logger.debug(f"[{self.name}] called list_tools")
800
+
773
801
  result = await self.session.list_tools()
774
802
  return result
775
803
 
@@ -812,9 +840,10 @@ class Client(Generic[ClientTransportT]):
812
840
  Raises:
813
841
  RuntimeError: If called while the client is not connected.
814
842
  """
843
+ logger.debug(f"[{self.name}] called call_tool: {name}")
815
844
 
816
845
  if isinstance(timeout, int | float):
817
- timeout = datetime.timedelta(seconds=timeout)
846
+ timeout = datetime.timedelta(seconds=float(timeout))
818
847
  result = await self.session.call_tool(
819
848
  name=name,
820
849
  arguments=arguments,
@@ -884,7 +913,7 @@ class Client(Generic[ClientTransportT]):
884
913
  else:
885
914
  data = result.structuredContent
886
915
  except Exception as e:
887
- logger.error(f"Error parsing structured content: {e}")
916
+ logger.error(f"[{self.name}] Error parsing structured content: {e}")
888
917
 
889
918
  return CallToolResult(
890
919
  content=result.content,
@@ -893,6 +922,14 @@ class Client(Generic[ClientTransportT]):
893
922
  is_error=result.isError,
894
923
  )
895
924
 
925
+ @classmethod
926
+ def generate_name(cls, name: str | None = None) -> str:
927
+ class_name = cls.__name__
928
+ if name is None:
929
+ return f"{class_name}-{secrets.token_hex(2)}"
930
+ else:
931
+ return f"{class_name}-{name}-{secrets.token_hex(2)}"
932
+
896
933
 
897
934
  @dataclass
898
935
  class CallToolResult:
fastmcp/client/logging.py CHANGED
@@ -13,7 +13,31 @@ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
13
13
 
14
14
 
15
15
  async def default_log_handler(message: LogMessage) -> None:
16
- logger.debug(f"Log received: {message}")
16
+ """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
+
20
+ # Map MCP log levels to Python logging levels
21
+ 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
30
+ }
31
+
32
+ # Get the appropriate logging function based on the message level
33
+ log_fn = level_map.get(message.level.lower(), logger.info)
34
+
35
+ # Include logger name if available
36
+ if message.logger:
37
+ msg = f"[{message.logger}] {msg}"
38
+
39
+ # Log with appropriate level and extra data
40
+ log_fn(f"Server log: {msg}", extra=extra)
17
41
 
18
42
 
19
43
  def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
@@ -29,17 +29,32 @@ def create_callback_html(
29
29
  server_url: str | None = None,
30
30
  ) -> str:
31
31
  """Create a styled HTML response for OAuth callbacks."""
32
- status_emoji = "" if is_success else "❌"
33
- status_color = "#10b981" if is_success else "#ef4444" # emerald-500 / red-500
34
-
35
- # Add server info for success cases
36
- server_info = ""
32
+ logo_url = "https://gofastmcp.com/assets/brand/blue-logo.png"
33
+
34
+ # 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"
43
+
44
+ # Add detail info box for both success and error cases
45
+ detail_info = ""
37
46
  if is_success and server_url:
38
- server_info = f"""
39
- <div class="server-info">
47
+ detail_info = f"""
48
+ <div class="info-box">
40
49
  Connected to: <strong>{server_url}</strong>
41
50
  </div>
42
51
  """
52
+ elif not is_success:
53
+ detail_info = f"""
54
+ <div class="info-box error">
55
+ {message}
56
+ </div>
57
+ """
43
58
 
44
59
  return f"""
45
60
  <!DOCTYPE html>
@@ -49,127 +64,112 @@ def create_callback_html(
49
64
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
50
65
  <title>{title}</title>
51
66
  <style>
67
+ * {{
68
+ margin: 0;
69
+ padding: 0;
70
+ box-sizing: border-box;
71
+ }}
72
+
52
73
  body {{
53
- font-family: 'SF Mono', 'Monaco', 'Consolas', 'Roboto Mono', monospace;
74
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
54
75
  margin: 0;
55
76
  padding: 0;
56
77
  min-height: 100vh;
57
78
  display: flex;
58
79
  align-items: center;
59
80
  justify-content: center;
60
- background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 25%, #16213e 50%, #0f0f23 100%);
61
- color: #e2e8f0;
62
- overflow: hidden;
63
- }}
64
-
65
- body::before {{
66
- content: '';
67
- position: fixed;
68
- top: 0;
69
- left: 0;
70
- width: 100%;
71
- height: 100%;
72
- background:
73
- radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
74
- radial-gradient(circle at 80% 20%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
75
- radial-gradient(circle at 40% 40%, rgba(14, 165, 233, 0.1) 0%, transparent 50%);
76
- pointer-events: none;
77
- z-index: -1;
81
+ background: #ffffff;
82
+ color: #0a0a0a;
78
83
  }}
79
84
 
80
85
  .container {{
81
- background: rgba(30, 41, 59, 0.9);
82
- backdrop-filter: blur(10px);
83
- border: 1px solid rgba(71, 85, 105, 0.3);
86
+ background: #ffffff;
87
+ border: 1px solid #e5e5e5;
84
88
  padding: 3rem 2rem;
85
- border-radius: 1rem;
86
- box-shadow:
87
- 0 25px 50px -12px rgba(0, 0, 0, 0.7),
88
- 0 0 0 1px rgba(255, 255, 255, 0.05),
89
- inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
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);
90
91
  text-align: center;
91
- max-width: 500px;
92
+ max-width: 28rem;
92
93
  margin: 1rem;
93
94
  position: relative;
94
95
  }}
95
96
 
96
- .container::before {{
97
- content: '';
98
- position: absolute;
99
- top: 0;
100
- left: 0;
101
- right: 0;
102
- height: 1px;
103
- background: linear-gradient(90deg, transparent, rgba(16, 185, 129, 0.5), transparent);
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;
104
112
  }}
105
113
 
106
114
  .status-icon {{
107
- font-size: 4rem;
108
- margin-bottom: 1rem;
109
- display: block;
110
- filter: drop-shadow(0 0 20px currentColor);
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;
111
125
  }}
112
126
 
113
127
  .message {{
114
- font-size: 1.25rem;
115
- line-height: 1.6;
116
- color: {status_color};
117
- margin-bottom: 1.5rem;
128
+ font-size: 1.125rem;
129
+ line-height: 1.75;
130
+ color: #0a0a0a;
118
131
  font-weight: 600;
119
- text-shadow: 0 0 10px rgba({
120
- "16, 185, 129" if is_success else "239, 68, 68"
121
- }, 0.3);
132
+ text-align: left;
122
133
  }}
123
134
 
124
- .server-info {{
125
- background: rgba(6, 182, 212, 0.1);
126
- border: 1px solid rgba(6, 182, 212, 0.3);
127
- border-radius: 0.75rem;
128
- padding: 1rem;
129
- margin: 1rem 0;
130
- font-size: 0.9rem;
131
- color: #67e8f9;
132
- font-family: 'SF Mono', 'Monaco', 'Consolas', 'Roboto Mono', monospace;
133
- text-shadow: 0 0 10px rgba(103, 232, 249, 0.3);
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;
134
145
  }}
135
146
 
136
- .server-info strong {{
137
- color: #22d3ee;
138
- font-weight: 700;
147
+ .info-box.error {{
148
+ background: #fef2f2;
149
+ border-color: #fecaca;
150
+ color: #991b1b;
139
151
  }}
140
152
 
141
- .subtitle {{
142
- font-size: 1rem;
143
- color: #94a3b8;
144
- margin-top: 1rem;
153
+ .info-box strong {{
154
+ color: #0a0a0a;
155
+ font-weight: 600;
145
156
  }}
146
157
 
147
158
  .close-instruction {{
148
- background: rgba(51, 65, 85, 0.8);
149
- border: 1px solid rgba(71, 85, 105, 0.4);
150
- border-radius: 0.75rem;
151
- padding: 1rem;
159
+ font-size: 0.875rem;
160
+ color: #737373;
152
161
  margin-top: 1.5rem;
153
- font-size: 0.9rem;
154
- color: #cbd5e1;
155
- font-family: 'SF Mono', 'Monaco', 'Consolas', 'Roboto Mono', monospace;
156
- }}
157
-
158
- @keyframes glow {{
159
- 0%, 100% {{ opacity: 1; }}
160
- 50% {{ opacity: 0.7; }}
161
- }}
162
-
163
- .status-icon {{
164
- animation: glow 2s ease-in-out infinite;
165
162
  }}
166
163
  </style>
167
164
  </head>
168
165
  <body>
169
166
  <div class="container">
170
- <span class="status-icon">{status_emoji}</span>
171
- <div class="message">{message}</div>
172
- {server_info}
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>
172
+ {detail_info}
173
173
  <div class="close-instruction">
174
174
  You can safely close this tab now.
175
175
  </div>
@@ -277,7 +277,7 @@ def create_oauth_callback_server(
277
277
  )
278
278
 
279
279
  return HTMLResponse(
280
- create_callback_html("FastMCP OAuth login complete!", server_url=server_url)
280
+ create_callback_html("", is_success=True, server_url=server_url)
281
281
  )
282
282
 
283
283
  app = Starlette(routes=[Route(callback_path, callback_handler)])
@@ -3,16 +3,18 @@ from collections.abc import Awaitable, Callable
3
3
  from typing import TypeAlias
4
4
 
5
5
  import mcp.types
6
- from mcp import ClientSession, CreateMessageResult
7
- from mcp.client.session import SamplingFnT
6
+ from mcp import CreateMessageResult
7
+ from mcp.client.session import ClientSession, SamplingFnT
8
8
  from mcp.shared.context import LifespanContextT, RequestContext
9
9
  from mcp.types import CreateMessageRequestParams as SamplingParams
10
10
  from mcp.types import SamplingMessage
11
11
 
12
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
13
+
12
14
  __all__ = ["SamplingMessage", "SamplingParams", "SamplingHandler"]
13
15
 
14
16
 
15
- SamplingHandler: TypeAlias = Callable[
17
+ ClientSamplingHandler: TypeAlias = Callable[
16
18
  [
17
19
  list[SamplingMessage],
18
20
  SamplingParams,
@@ -21,8 +23,14 @@ SamplingHandler: TypeAlias = Callable[
21
23
  str | CreateMessageResult | Awaitable[str | CreateMessageResult],
22
24
  ]
23
25
 
26
+ SamplingHandler: TypeAlias = (
27
+ ClientSamplingHandler[LifespanContextT] | ServerSamplingHandler[LifespanContextT]
28
+ )
29
+
24
30
 
25
- def create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT:
31
+ def create_sampling_callback(
32
+ sampling_handler: ClientSamplingHandler[LifespanContextT],
33
+ ) -> SamplingFnT:
26
34
  async def _sampling_handler(
27
35
  context: RequestContext[ClientSession, LifespanContextT],
28
36
  params: SamplingParams,