fastmcp 2.12.1__py3-none-any.whl → 2.13.2__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 (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  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 +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -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 +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/client.py CHANGED
@@ -61,15 +61,15 @@ from .transports import (
61
61
 
62
62
  __all__ = [
63
63
  "Client",
64
- "SessionKwargs",
65
- "RootsHandler",
66
- "RootsList",
67
- "LogHandler",
68
- "MessageHandler",
69
64
  "ClientSamplingHandler",
70
- "SamplingHandler",
71
65
  "ElicitationHandler",
66
+ "LogHandler",
67
+ "MessageHandler",
72
68
  "ProgressHandler",
69
+ "RootsHandler",
70
+ "RootsList",
71
+ "SamplingHandler",
72
+ "SessionKwargs",
73
73
  ]
74
74
 
75
75
  logger = get_logger(__name__)
@@ -77,6 +77,16 @@ logger = get_logger(__name__)
77
77
  T = TypeVar("T", bound="ClientTransport")
78
78
 
79
79
 
80
+ def _timeout_to_seconds(
81
+ timeout: datetime.timedelta | float | int | None,
82
+ ) -> float | None:
83
+ if timeout is None:
84
+ return None
85
+ if isinstance(timeout, datetime.timedelta):
86
+ return timeout.total_seconds()
87
+ return float(timeout)
88
+
89
+
80
90
  @dataclass
81
91
  class ClientSessionState:
82
92
  """Holds all session-related state for a Client instance.
@@ -155,38 +165,38 @@ class Client(Generic[ClientTransportT]):
155
165
  """
156
166
 
157
167
  @overload
158
- def __init__(self: Client[T], transport: T, *args, **kwargs) -> None: ...
168
+ def __init__(self: Client[T], transport: T, *args: Any, **kwargs: Any) -> None: ...
159
169
 
160
170
  @overload
161
171
  def __init__(
162
172
  self: Client[SSETransport | StreamableHttpTransport],
163
173
  transport: AnyUrl,
164
- *args,
165
- **kwargs,
174
+ *args: Any,
175
+ **kwargs: Any,
166
176
  ) -> None: ...
167
177
 
168
178
  @overload
169
179
  def __init__(
170
180
  self: Client[FastMCPTransport],
171
181
  transport: FastMCP | FastMCP1Server,
172
- *args,
173
- **kwargs,
182
+ *args: Any,
183
+ **kwargs: Any,
174
184
  ) -> None: ...
175
185
 
176
186
  @overload
177
187
  def __init__(
178
188
  self: Client[PythonStdioTransport | NodeStdioTransport],
179
189
  transport: Path,
180
- *args,
181
- **kwargs,
190
+ *args: Any,
191
+ **kwargs: Any,
182
192
  ) -> None: ...
183
193
 
184
194
  @overload
185
195
  def __init__(
186
196
  self: Client[MCPConfigTransport],
187
197
  transport: MCPConfig | dict[str, Any],
188
- *args,
189
- **kwargs,
198
+ *args: Any,
199
+ **kwargs: Any,
190
200
  ) -> None: ...
191
201
 
192
202
  @overload
@@ -198,8 +208,8 @@ class Client(Generic[ClientTransportT]):
198
208
  | StreamableHttpTransport
199
209
  ],
200
210
  transport: str,
201
- *args,
202
- **kwargs,
211
+ *args: Any,
212
+ **kwargs: Any,
203
213
  ) -> None: ...
204
214
 
205
215
  def __init__(
@@ -222,6 +232,7 @@ class Client(Generic[ClientTransportT]):
222
232
  message_handler: MessageHandlerT | MessageHandler | None = None,
223
233
  progress_handler: ProgressHandler | None = None,
224
234
  timeout: datetime.timedelta | float | int | None = None,
235
+ auto_initialize: bool = True,
225
236
  init_timeout: datetime.timedelta | float | int | None = None,
226
237
  client_info: mcp.types.Implementation | None = None,
227
238
  auth: httpx.Auth | Literal["oauth"] | str | None = None,
@@ -240,26 +251,23 @@ class Client(Generic[ClientTransportT]):
240
251
 
241
252
  self._progress_handler = progress_handler
242
253
 
254
+ # Convert timeout to timedelta if needed
243
255
  if isinstance(timeout, int | float):
244
256
  timeout = datetime.timedelta(seconds=float(timeout))
245
257
 
246
258
  # handle init handshake timeout
247
259
  if init_timeout is None:
248
260
  init_timeout = fastmcp.settings.client_init_timeout
249
- if isinstance(init_timeout, datetime.timedelta):
250
- init_timeout = init_timeout.total_seconds()
251
- elif not init_timeout:
252
- init_timeout = None
253
- else:
254
- init_timeout = float(init_timeout)
255
- self._init_timeout = init_timeout
261
+ self._init_timeout = _timeout_to_seconds(init_timeout)
262
+
263
+ self.auto_initialize = auto_initialize
256
264
 
257
265
  self._session_kwargs: SessionKwargs = {
258
266
  "sampling_callback": None,
259
267
  "list_roots_callback": None,
260
268
  "logging_callback": create_log_callback(log_handler),
261
269
  "message_handler": message_handler,
262
- "read_timeout_seconds": timeout,
270
+ "read_timeout_seconds": timeout, # ty: ignore[invalid-argument-type]
263
271
  "client_info": client_info,
264
272
  }
265
273
 
@@ -290,12 +298,8 @@ class Client(Generic[ClientTransportT]):
290
298
  return self._session_state.session
291
299
 
292
300
  @property
293
- def initialize_result(self) -> mcp.types.InitializeResult:
301
+ def initialize_result(self) -> mcp.types.InitializeResult | None:
294
302
  """Get the result of the initialization request."""
295
- if self._session_state.initialize_result is None:
296
- raise RuntimeError(
297
- "Client is not connected. Use the 'async with client:' context manager first."
298
- )
299
303
  return self._session_state.initialize_result
300
304
 
301
305
  def set_roots(self, roots: RootsList | RootsHandler) -> None:
@@ -357,15 +361,11 @@ class Client(Generic[ClientTransportT]):
357
361
  self._session_state.session = session
358
362
  # Initialize the session
359
363
  try:
360
- with anyio.fail_after(self._init_timeout):
361
- self._session_state.initialize_result = (
362
- await self._session_state.session.initialize()
363
- )
364
+ if self.auto_initialize:
365
+ await self.initialize()
364
366
  yield
365
- except anyio.ClosedResourceError:
366
- raise RuntimeError("Server session was closed unexpectedly")
367
- except TimeoutError:
368
- raise RuntimeError("Failed to initialize server session")
367
+ except anyio.ClosedResourceError as e:
368
+ raise RuntimeError("Server session was closed unexpectedly") from e
369
369
  finally:
370
370
  self._session_state.session = None
371
371
  self._session_state.initialize_result = None
@@ -454,7 +454,7 @@ class Client(Generic[ClientTransportT]):
454
454
  if self._session_state.nesting_counter > 0:
455
455
  return
456
456
 
457
- # stop the active seesion
457
+ # stop the active session
458
458
  if self._session_state.session_task is None:
459
459
  return
460
460
  self._session_state.stop_event.set()
@@ -493,6 +493,55 @@ class Client(Generic[ClientTransportT]):
493
493
 
494
494
  # --- MCP Client Methods ---
495
495
 
496
+ async def initialize(
497
+ self,
498
+ timeout: datetime.timedelta | float | int | None = None,
499
+ ) -> mcp.types.InitializeResult:
500
+ """Send an initialize request to the server.
501
+
502
+ This method performs the MCP initialization handshake with the server,
503
+ exchanging capabilities and server information. It is idempotent - calling
504
+ it multiple times returns the cached result from the first call.
505
+
506
+ The initialization happens automatically when entering the client context
507
+ manager unless `auto_initialize=False` was set during client construction.
508
+ Manual calls to this method are only needed when auto-initialization is disabled.
509
+
510
+ Args:
511
+ timeout: Optional timeout for the initialization request (seconds or timedelta).
512
+ If None, uses the client's init_timeout setting.
513
+
514
+ Returns:
515
+ InitializeResult: The server's initialization response containing server info,
516
+ capabilities, protocol version, and optional instructions.
517
+
518
+ Raises:
519
+ RuntimeError: If the client is not connected or initialization times out.
520
+
521
+ Example:
522
+ ```python
523
+ # With auto-initialization disabled
524
+ client = Client(server, auto_initialize=False)
525
+ async with client:
526
+ result = await client.initialize()
527
+ print(f"Server: {result.serverInfo.name}")
528
+ print(f"Instructions: {result.instructions}")
529
+ ```
530
+ """
531
+
532
+ if self.initialize_result is not None:
533
+ return self.initialize_result
534
+
535
+ if timeout is None:
536
+ timeout = self._init_timeout
537
+ try:
538
+ with anyio.fail_after(_timeout_to_seconds(timeout)):
539
+ initialize_result = await self.session.initialize()
540
+ self._session_state.initialize_result = initialize_result
541
+ return initialize_result
542
+ except TimeoutError as e:
543
+ raise RuntimeError("Failed to initialize server session") from e
544
+
496
545
  async def ping(self) -> bool:
497
546
  """Send a ping request."""
498
547
  result = await self.session.send_ping()
@@ -505,7 +554,7 @@ class Client(Generic[ClientTransportT]):
505
554
  ) -> None:
506
555
  """Send a cancellation notification for an in-progress request."""
507
556
  notification = mcp.types.ClientNotification(
508
- mcp.types.CancelledNotification(
557
+ root=mcp.types.CancelledNotification(
509
558
  method="notifications/cancelled",
510
559
  params=mcp.types.CancelledNotificationParams(
511
560
  requestId=request_id,
@@ -743,14 +792,17 @@ class Client(Generic[ClientTransportT]):
743
792
 
744
793
  async def complete_mcp(
745
794
  self,
746
- ref: mcp.types.ResourceReference | mcp.types.PromptReference,
795
+ ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference,
747
796
  argument: dict[str, str],
797
+ context_arguments: dict[str, Any] | None = None,
748
798
  ) -> mcp.types.CompleteResult:
749
799
  """Send a completion request and return the complete MCP protocol result.
750
800
 
751
801
  Args:
752
- ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
802
+ ref (mcp.types.ResourceTemplateReference | mcp.types.PromptReference): The reference to complete.
753
803
  argument (dict[str, str]): Arguments to pass to the completion request.
804
+ context_arguments (dict[str, Any] | None, optional): Optional context arguments to
805
+ include with the completion request. Defaults to None.
754
806
 
755
807
  Returns:
756
808
  mcp.types.CompleteResult: The complete response object from the protocol,
@@ -761,19 +813,24 @@ class Client(Generic[ClientTransportT]):
761
813
  """
762
814
  logger.debug(f"[{self.name}] called complete: {ref}")
763
815
 
764
- result = await self.session.complete(ref=ref, argument=argument)
816
+ result = await self.session.complete(
817
+ ref=ref, argument=argument, context_arguments=context_arguments
818
+ )
765
819
  return result
766
820
 
767
821
  async def complete(
768
822
  self,
769
- ref: mcp.types.ResourceReference | mcp.types.PromptReference,
823
+ ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference,
770
824
  argument: dict[str, str],
825
+ context_arguments: dict[str, Any] | None = None,
771
826
  ) -> mcp.types.Completion:
772
827
  """Send a completion request to the server.
773
828
 
774
829
  Args:
775
- ref (mcp.types.ResourceReference | mcp.types.PromptReference): The reference to complete.
830
+ ref (mcp.types.ResourceTemplateReference | mcp.types.PromptReference): The reference to complete.
776
831
  argument (dict[str, str]): Arguments to pass to the completion request.
832
+ context_arguments (dict[str, Any] | None, optional): Optional context arguments to
833
+ include with the completion request. Defaults to None.
777
834
 
778
835
  Returns:
779
836
  mcp.types.Completion: The completion object.
@@ -781,7 +838,9 @@ class Client(Generic[ClientTransportT]):
781
838
  Raises:
782
839
  RuntimeError: If called while the client is not connected.
783
840
  """
784
- result = await self.complete_mcp(ref=ref, argument=argument)
841
+ result = await self.complete_mcp(
842
+ ref=ref, argument=argument, context_arguments=context_arguments
843
+ )
785
844
  return result.completion
786
845
 
787
846
  # --- Tools ---
@@ -821,6 +880,7 @@ class Client(Generic[ClientTransportT]):
821
880
  arguments: dict[str, Any],
822
881
  progress_handler: ProgressHandler | None = None,
823
882
  timeout: datetime.timedelta | float | int | None = None,
883
+ meta: dict[str, Any] | None = None,
824
884
  ) -> mcp.types.CallToolResult:
825
885
  """Send a tools/call request and return the complete MCP protocol result.
826
886
 
@@ -832,6 +892,10 @@ class Client(Generic[ClientTransportT]):
832
892
  arguments (dict[str, Any]): Arguments to pass to the tool.
833
893
  timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
834
894
  progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
895
+ meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
896
+ This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
897
+ that shouldn't be tool arguments but may influence server-side processing. The server
898
+ can access this via `context.request_context.meta`. Defaults to None.
835
899
 
836
900
  Returns:
837
901
  mcp.types.CallToolResult: The complete response object from the protocol,
@@ -842,13 +906,16 @@ class Client(Generic[ClientTransportT]):
842
906
  """
843
907
  logger.debug(f"[{self.name}] called call_tool: {name}")
844
908
 
909
+ # Convert timeout to timedelta if needed
845
910
  if isinstance(timeout, int | float):
846
911
  timeout = datetime.timedelta(seconds=float(timeout))
912
+
847
913
  result = await self.session.call_tool(
848
914
  name=name,
849
915
  arguments=arguments,
850
- read_timeout_seconds=timeout,
916
+ read_timeout_seconds=timeout, # ty: ignore[invalid-argument-type]
851
917
  progress_callback=progress_handler or self._progress_handler,
918
+ meta=meta,
852
919
  )
853
920
  return result
854
921
 
@@ -859,6 +926,7 @@ class Client(Generic[ClientTransportT]):
859
926
  timeout: datetime.timedelta | float | int | None = None,
860
927
  progress_handler: ProgressHandler | None = None,
861
928
  raise_on_error: bool = True,
929
+ meta: dict[str, Any] | None = None,
862
930
  ) -> CallToolResult:
863
931
  """Call a tool on the server.
864
932
 
@@ -869,6 +937,11 @@ class Client(Generic[ClientTransportT]):
869
937
  arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
870
938
  timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
871
939
  progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
940
+ raise_on_error (bool, optional): Whether to raise a ToolError if the tool call results in an error. Defaults to True.
941
+ meta (dict[str, Any] | None, optional): Additional metadata to include with the request.
942
+ This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
943
+ that shouldn't be tool arguments but may influence server-side processing. The server
944
+ can access this via `context.request_context.meta`. Defaults to None.
872
945
 
873
946
  Returns:
874
947
  CallToolResult:
@@ -888,6 +961,7 @@ class Client(Generic[ClientTransportT]):
888
961
  arguments=arguments or {},
889
962
  timeout=timeout,
890
963
  progress_handler=progress_handler,
964
+ meta=meta,
891
965
  )
892
966
  data = None
893
967
  if result.isError and raise_on_error:
@@ -918,6 +992,7 @@ class Client(Generic[ClientTransportT]):
918
992
  return CallToolResult(
919
993
  content=result.content,
920
994
  structured_content=result.structuredContent,
995
+ meta=result.meta,
921
996
  data=data,
922
997
  is_error=result.isError,
923
998
  )
@@ -935,5 +1010,6 @@ class Client(Generic[ClientTransportT]):
935
1010
  class CallToolResult:
936
1011
  content: list[mcp.types.ContentBlock]
937
1012
  structured_content: dict[str, Any] | None
1013
+ meta: dict[str, Any] | None
938
1014
  data: Any = None
939
1015
  is_error: bool = False
@@ -59,7 +59,12 @@ def create_elicitation_callback(
59
59
  "Elicitation responses must be serializable as a JSON object (dict). Received: "
60
60
  f"{result.content!r}"
61
61
  )
62
- return MCPElicitResult(**result.model_dump() | {"content": content})
62
+ return MCPElicitResult(
63
+ _meta=result.meta,
64
+ action=result.action,
65
+ content=content,
66
+ )
67
+
63
68
  except Exception as e:
64
69
  return mcp.types.ErrorData(
65
70
  code=mcp.types.INTERNAL_ERROR,
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: