fastmcp 2.3.3__py3-none-any.whl → 2.3.4__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.
fastmcp/server/proxy.py CHANGED
@@ -18,7 +18,7 @@ from mcp.types import (
18
18
  from pydantic.networks import AnyUrl
19
19
 
20
20
  from fastmcp.client import Client
21
- from fastmcp.exceptions import NotFoundError
21
+ from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
22
22
  from fastmcp.prompts import Prompt, PromptMessage
23
23
  from fastmcp.resources import Resource, ResourceTemplate
24
24
  from fastmcp.server.context import Context
@@ -64,7 +64,7 @@ class ProxyTool(Tool):
64
64
  arguments=arguments,
65
65
  )
66
66
  if result.isError:
67
- raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
67
+ raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
68
68
  return result.content
69
69
 
70
70
 
@@ -97,7 +97,7 @@ class ProxyResource(Resource):
97
97
  elif isinstance(result[0], BlobResourceContents):
98
98
  return result[0].blob
99
99
  else:
100
- raise ValueError(f"Unsupported content type: {type(result[0])}")
100
+ raise ResourceError(f"Unsupported content type: {type(result[0])}")
101
101
 
102
102
 
103
103
  class ProxyTemplate(ResourceTemplate):
@@ -138,7 +138,7 @@ class ProxyTemplate(ResourceTemplate):
138
138
  elif isinstance(result[0], BlobResourceContents):
139
139
  value = result[0].blob
140
140
  else:
141
- raise ValueError(f"Unsupported content type: {type(result[0])}")
141
+ raise ResourceError(f"Unsupported content type: {type(result[0])}")
142
142
 
143
143
  return ProxyResource(
144
144
  client=self._client,
fastmcp/server/server.py CHANGED
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import datetime
6
- import inspect
7
6
  import warnings
8
7
  from collections.abc import AsyncIterator, Awaitable, Callable
9
8
  from contextlib import (
@@ -20,7 +19,7 @@ import pydantic
20
19
  import uvicorn
21
20
  from mcp.server.auth.provider import OAuthAuthorizationServerProvider
22
21
  from mcp.server.lowlevel.helper_types import ReadResourceContents
23
- from mcp.server.lowlevel.server import LifespanResultT
22
+ from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
24
23
  from mcp.server.lowlevel.server import Server as MCPServer
25
24
  from mcp.server.stdio import stdio_server
26
25
  from mcp.types import (
@@ -44,7 +43,7 @@ from starlette.routing import BaseRoute, Route
44
43
 
45
44
  import fastmcp.server
46
45
  import fastmcp.settings
47
- from fastmcp.exceptions import NotFoundError, ResourceError
46
+ from fastmcp.exceptions import NotFoundError
48
47
  from fastmcp.prompts import Prompt, PromptManager
49
48
  from fastmcp.prompts.prompt import PromptResult
50
49
  from fastmcp.resources import Resource, ResourceManager
@@ -54,7 +53,7 @@ from fastmcp.tools import ToolManager
54
53
  from fastmcp.tools.tool import Tool
55
54
  from fastmcp.utilities.cache import TimedCache
56
55
  from fastmcp.utilities.decorators import DecoratedFunction
57
- from fastmcp.utilities.logging import configure_logging, get_logger
56
+ from fastmcp.utilities.logging import get_logger
58
57
 
59
58
  if TYPE_CHECKING:
60
59
  from fastmcp.client import Client
@@ -63,6 +62,8 @@ if TYPE_CHECKING:
63
62
 
64
63
  logger = get_logger(__name__)
65
64
 
65
+ DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
66
+
66
67
 
67
68
  @asynccontextmanager
68
69
  async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
@@ -107,40 +108,52 @@ class FastMCP(Generic[LifespanResultT]):
107
108
  | None
108
109
  ) = None,
109
110
  tags: set[str] | None = None,
111
+ dependencies: list[str] | None = None,
110
112
  tool_serializer: Callable[[Any], str] | None = None,
113
+ cache_expiration_seconds: float | None = None,
114
+ on_duplicate_tools: DuplicateBehavior | None = None,
115
+ on_duplicate_resources: DuplicateBehavior | None = None,
116
+ on_duplicate_prompts: DuplicateBehavior | None = None,
111
117
  **settings: Any,
112
118
  ):
113
- self.tags: set[str] = tags or set()
119
+ if settings:
120
+ # TODO: remove settings. Deprecated since 2.3.4
121
+ warnings.warn(
122
+ "Passing runtime and transport-specific settings as kwargs "
123
+ "to the FastMCP constructor is deprecated (as of 2.3.4), "
124
+ "including most transport settings. If possible, provide settings when calling "
125
+ "run() instead.",
126
+ DeprecationWarning,
127
+ stacklevel=2,
128
+ )
114
129
  self.settings = fastmcp.settings.ServerSettings(**settings)
130
+
131
+ self.tags: set[str] = tags or set()
132
+ self.dependencies = dependencies
115
133
  self._cache = TimedCache(
116
- expiration=datetime.timedelta(
117
- seconds=self.settings.cache_expiration_seconds
118
- )
134
+ expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
119
135
  )
120
-
121
136
  self._mounted_servers: dict[str, MountedServer] = {}
137
+ self._additional_http_routes: list[BaseRoute] = []
138
+ self._tool_manager = ToolManager(
139
+ duplicate_behavior=on_duplicate_tools,
140
+ serializer=tool_serializer,
141
+ )
142
+ self._resource_manager = ResourceManager(
143
+ duplicate_behavior=on_duplicate_resources
144
+ )
145
+ self._prompt_manager = PromptManager(duplicate_behavior=on_duplicate_prompts)
122
146
 
123
147
  if lifespan is None:
124
148
  self._has_lifespan = False
125
149
  lifespan = default_lifespan
126
150
  else:
127
151
  self._has_lifespan = True
128
-
129
152
  self._mcp_server = MCPServer[LifespanResultT](
130
153
  name=name or "FastMCP",
131
154
  instructions=instructions,
132
155
  lifespan=_lifespan_wrapper(self, lifespan),
133
156
  )
134
- self._tool_manager = ToolManager(
135
- duplicate_behavior=self.settings.on_duplicate_tools,
136
- serializer=tool_serializer,
137
- )
138
- self._resource_manager = ResourceManager(
139
- duplicate_behavior=self.settings.on_duplicate_resources
140
- )
141
- self._prompt_manager = PromptManager(
142
- duplicate_behavior=self.settings.on_duplicate_prompts
143
- )
144
157
 
145
158
  if (self.settings.auth is not None) != (auth_server_provider is not None):
146
159
  # TODO: after we support separate authorization servers (see
@@ -150,15 +163,9 @@ class FastMCP(Generic[LifespanResultT]):
150
163
  )
151
164
  self._auth_server_provider = auth_server_provider
152
165
 
153
- self._additional_http_routes: list[BaseRoute] = []
154
- self.dependencies = self.settings.dependencies
155
-
156
166
  # Set up MCP protocol handlers
157
167
  self._setup_handlers()
158
168
 
159
- # Configure logging
160
- configure_logging(self.settings.log_level)
161
-
162
169
  def __repr__(self) -> str:
163
170
  return f"{type(self).__name__}({self.name!r})"
164
171
 
@@ -378,16 +385,13 @@ class FastMCP(Generic[LifespanResultT]):
378
385
  with fastmcp.server.context.Context(fastmcp=self):
379
386
  if self._resource_manager.has_resource(uri):
380
387
  resource = await self._resource_manager.get_resource(uri)
381
- try:
382
- content = await resource.read()
383
- return [
384
- ReadResourceContents(
385
- content=content, mime_type=resource.mime_type
386
- )
387
- ]
388
- except Exception as e:
389
- logger.error(f"Error reading resource {uri}: {e}")
390
- raise ResourceError(str(e))
388
+ content = await self._resource_manager.read_resource(uri)
389
+ return [
390
+ ReadResourceContents(
391
+ content=content,
392
+ mime_type=resource.mime_type,
393
+ )
394
+ ]
391
395
  else:
392
396
  for server in self._mounted_servers.values():
393
397
  if server.match_resource(str(uri)):
@@ -414,7 +418,7 @@ class FastMCP(Generic[LifespanResultT]):
414
418
  for server in self._mounted_servers.values():
415
419
  if server.match_prompt(name):
416
420
  new_key = server.strip_prompt_prefix(name)
417
- return await server.server._mcp_get_prompt(new_key, arguments)
421
+ return await server.server._mcp_get_prompt(new_key, arguments)
418
422
  else:
419
423
  raise NotFoundError(f"Unknown prompt: {name}")
420
424
 
@@ -450,6 +454,18 @@ class FastMCP(Generic[LifespanResultT]):
450
454
  )
451
455
  self._cache.clear()
452
456
 
457
+ def remove_tool(self, name: str) -> None:
458
+ """Remove a tool from the server.
459
+
460
+ Args:
461
+ name: The name of the tool to remove
462
+
463
+ Raises:
464
+ NotFoundError: If the tool is not found
465
+ """
466
+ self._tool_manager.remove_tool(name)
467
+ self._cache.clear()
468
+
453
469
  def tool(
454
470
  self,
455
471
  name: str | None = None,
@@ -715,7 +731,9 @@ class FastMCP(Generic[LifespanResultT]):
715
731
  await self._mcp_server.run(
716
732
  read_stream,
717
733
  write_stream,
718
- self._mcp_server.create_initialization_options(),
734
+ self._mcp_server.create_initialization_options(
735
+ NotificationOptions(tools_changed=True)
736
+ ),
719
737
  )
720
738
 
721
739
  async def run_http_async(
@@ -764,15 +782,14 @@ class FastMCP(Generic[LifespanResultT]):
764
782
  uvicorn_config: dict | None = None,
765
783
  ) -> None:
766
784
  """Run the server using SSE transport."""
785
+
786
+ # Deprecated since 2.3.2
767
787
  warnings.warn(
768
- inspect.cleandoc(
769
- """
770
- The run_sse_async method is deprecated. Use run_http_async for a
771
- modern (non-SSE) alternative, or create an SSE app with
772
- `fastmcp.server.http.create_sse_app` and run it directly.
773
- """
774
- ),
788
+ "The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
789
+ "modern (non-SSE) alternative, or create an SSE app with "
790
+ "`fastmcp.server.http.create_sse_app` and run it directly.",
775
791
  DeprecationWarning,
792
+ stacklevel=2,
776
793
  )
777
794
  await self.run_http_async(
778
795
  transport="sse",
@@ -797,14 +814,12 @@ class FastMCP(Generic[LifespanResultT]):
797
814
  message_path: The path to the message endpoint
798
815
  middleware: A list of middleware to apply to the app
799
816
  """
817
+ # Deprecated since 2.3.2
800
818
  warnings.warn(
801
- inspect.cleandoc(
802
- """
803
- The sse_app method is deprecated. Use http_app as a modern (non-SSE)
804
- alternative, or call `fastmcp.server.http.create_sse_app` directly.
805
- """
806
- ),
819
+ "The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
820
+ "alternative, or call `fastmcp.server.http.create_sse_app` directly.",
807
821
  DeprecationWarning,
822
+ stacklevel=2,
808
823
  )
809
824
  return create_sse_app(
810
825
  server=self,
@@ -829,9 +844,11 @@ class FastMCP(Generic[LifespanResultT]):
829
844
  path: The path to the StreamableHTTP endpoint
830
845
  middleware: A list of middleware to apply to the app
831
846
  """
847
+ # Deprecated since 2.3.2
832
848
  warnings.warn(
833
- "The streamable_http_app method is deprecated. Use http_app() instead.",
849
+ "The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
834
850
  DeprecationWarning,
851
+ stacklevel=2,
835
852
  )
836
853
  return self.http_app(path=path, middleware=middleware)
837
854
 
@@ -886,9 +903,12 @@ class FastMCP(Generic[LifespanResultT]):
886
903
  path: str | None = None,
887
904
  uvicorn_config: dict | None = None,
888
905
  ) -> None:
906
+ # Deprecated since 2.3.2
889
907
  warnings.warn(
890
- "The run_streamable_http_async method is deprecated. Use run_http_async instead.",
908
+ "The run_streamable_http_async method is deprecated (as of 2.3.2). "
909
+ "Use run_http_async instead.",
891
910
  DeprecationWarning,
911
+ stacklevel=2,
892
912
  )
893
913
  await self.run_http_async(
894
914
  transport="streamable-http",
fastmcp/settings.py CHANGED
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- from typing import TYPE_CHECKING, Literal
3
+ import inspect
4
+ from typing import TYPE_CHECKING, Annotated, Literal
4
5
 
5
6
  from mcp.server.auth.settings import AuthSettings
6
- from pydantic import Field
7
+ from pydantic import Field, model_validator
7
8
  from pydantic_settings import BaseSettings, SettingsConfigDict
9
+ from typing_extensions import Self
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  pass
@@ -27,16 +29,46 @@ class Settings(BaseSettings):
27
29
 
28
30
  test_mode: bool = False
29
31
  log_level: LOG_LEVEL = "INFO"
30
- tool_attempt_parse_json_args: bool = Field(
31
- default=False,
32
- description="""
33
- Note: this enables a legacy behavior. If True, will attempt to parse
34
- stringified JSON lists and objects strings in tool arguments before
35
- passing them to the tool. This is an old behavior that can create
36
- unexpected type coercion issues, but may be helpful for less powerful
37
- LLMs that stringify JSON instead of passing actual lists and objects.
38
- Defaults to False.""",
39
- )
32
+ client_raise_first_exceptiongroup_error: Annotated[
33
+ bool,
34
+ Field(
35
+ default=True,
36
+ description=inspect.cleandoc(
37
+ """
38
+ Many MCP components operate in anyio taskgroups, and raise
39
+ ExceptionGroups instead of exceptions. If this setting is True, FastMCP Clients
40
+ will `raise` the first error in any ExceptionGroup instead of raising
41
+ the ExceptionGroup as a whole. This is useful for debugging, but may
42
+ mask other errors.
43
+ """
44
+ ),
45
+ ),
46
+ ] = True
47
+ tool_attempt_parse_json_args: Annotated[
48
+ bool,
49
+ Field(
50
+ default=False,
51
+ description=inspect.cleandoc(
52
+ """
53
+ Note: this enables a legacy behavior. If True, will attempt to parse
54
+ stringified JSON lists and objects strings in tool arguments before
55
+ passing them to the tool. This is an old behavior that can create
56
+ unexpected type coercion issues, but may be helpful for less powerful
57
+ LLMs that stringify JSON instead of passing actual lists and objects.
58
+ Defaults to False.
59
+ """
60
+ ),
61
+ ),
62
+ ] = False
63
+
64
+ @model_validator(mode="after")
65
+ def setup_logging(self) -> Self:
66
+ """Finalize the settings."""
67
+ from fastmcp.utilities.logging import configure_logging
68
+
69
+ configure_logging(self.log_level)
70
+
71
+ return self
40
72
 
41
73
 
42
74
  class ServerSettings(BaseSettings):
@@ -54,7 +86,10 @@ class ServerSettings(BaseSettings):
54
86
  nested_model_default_partial_update=True,
55
87
  )
56
88
 
57
- log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
89
+ log_level: Annotated[
90
+ LOG_LEVEL,
91
+ Field(default_factory=lambda: Settings().log_level),
92
+ ]
58
93
 
59
94
  # HTTP settings
60
95
  host: str = "127.0.0.1"
@@ -73,10 +108,13 @@ class ServerSettings(BaseSettings):
73
108
  # prompt settings
74
109
  on_duplicate_prompts: DuplicateBehavior = "warn"
75
110
 
76
- dependencies: list[str] = Field(
77
- default_factory=list,
78
- description="List of dependencies to install in the server environment",
79
- )
111
+ dependencies: Annotated[
112
+ list[str],
113
+ Field(
114
+ default_factory=list,
115
+ description="List of dependencies to install in the server environment",
116
+ ),
117
+ ] = []
80
118
 
81
119
  # cache settings (for checking mounted servers)
82
120
  cache_expiration_seconds: float = 0
@@ -90,16 +128,4 @@ class ServerSettings(BaseSettings):
90
128
  )
91
129
 
92
130
 
93
- class ClientSettings(BaseSettings):
94
- """FastMCP client settings."""
95
-
96
- model_config = SettingsConfigDict(
97
- env_prefix="FASTMCP_CLIENT_",
98
- env_file=".env",
99
- extra="ignore",
100
- )
101
-
102
- log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
103
-
104
-
105
131
  settings = Settings()
fastmcp/tools/tool.py CHANGED
@@ -11,9 +11,8 @@ from mcp.types import Tool as MCPTool
11
11
  from pydantic import BaseModel, BeforeValidator, Field
12
12
 
13
13
  import fastmcp
14
- from fastmcp.exceptions import ToolError
15
14
  from fastmcp.server.dependencies import get_context
16
- from fastmcp.utilities.json_schema import prune_params
15
+ from fastmcp.utilities.json_schema import compress_schema
17
16
  from fastmcp.utilities.logging import get_logger
18
17
  from fastmcp.utilities.types import (
19
18
  Image,
@@ -82,7 +81,11 @@ class Tool(BaseModel):
82
81
 
83
82
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
84
83
  if context_kwarg:
85
- schema = prune_params(schema, params=[context_kwarg])
84
+ prune_params = [context_kwarg]
85
+ else:
86
+ prune_params = None
87
+
88
+ schema = compress_schema(schema, prune_params=prune_params)
86
89
 
87
90
  return cls(
88
91
  fn=fn,
@@ -102,48 +105,45 @@ class Tool(BaseModel):
102
105
 
103
106
  arguments = arguments.copy()
104
107
 
105
- try:
106
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
107
- if context_kwarg and context_kwarg not in arguments:
108
- arguments[context_kwarg] = get_context()
109
-
110
- if fastmcp.settings.settings.tool_attempt_parse_json_args:
111
- # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
112
- # being passed in as JSON inside a string rather than an actual list.
113
- #
114
- # Claude desktop is prone to this - in fact it seems incapable of NOT doing
115
- # this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
116
- # which can be pre-parsed here.
117
- signature = inspect.signature(self.fn)
118
- for param_name in self.parameters["properties"]:
119
- arg = arguments.get(param_name, None)
120
- # if not in signature, we won't have annotations, so skip logic
121
- if param_name not in signature.parameters:
122
- continue
123
- # if not a string, we won't have a JSON to parse, so skip logic
124
- if not isinstance(arg, str):
125
- continue
126
- # skip if the type is a simple type (int, float, bool)
127
- if signature.parameters[param_name].annotation in (
128
- int,
129
- float,
130
- bool,
131
- ):
132
- continue
133
- try:
134
- arguments[param_name] = json.loads(arg)
135
-
136
- except json.JSONDecodeError:
137
- pass
138
-
139
- type_adapter = get_cached_typeadapter(self.fn)
140
- result = type_adapter.validate_python(arguments)
141
- if inspect.isawaitable(result):
142
- result = await result
143
-
144
- return _convert_to_content(result, serializer=self.serializer)
145
- except Exception as e:
146
- raise ToolError(f"Error executing tool {self.name}: {e}") from e
108
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
109
+ if context_kwarg and context_kwarg not in arguments:
110
+ arguments[context_kwarg] = get_context()
111
+
112
+ if fastmcp.settings.settings.tool_attempt_parse_json_args:
113
+ # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
114
+ # being passed in as JSON inside a string rather than an actual list.
115
+ #
116
+ # Claude desktop is prone to this - in fact it seems incapable of NOT doing
117
+ # this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
118
+ # which can be pre-parsed here.
119
+ signature = inspect.signature(self.fn)
120
+ for param_name in self.parameters["properties"]:
121
+ arg = arguments.get(param_name, None)
122
+ # if not in signature, we won't have annotations, so skip logic
123
+ if param_name not in signature.parameters:
124
+ continue
125
+ # if not a string, we won't have a JSON to parse, so skip logic
126
+ if not isinstance(arg, str):
127
+ continue
128
+ # skip if the type is a simple type (int, float, bool)
129
+ if signature.parameters[param_name].annotation in (
130
+ int,
131
+ float,
132
+ bool,
133
+ ):
134
+ continue
135
+ try:
136
+ arguments[param_name] = json.loads(arg)
137
+
138
+ except json.JSONDecodeError:
139
+ pass
140
+
141
+ type_adapter = get_cached_typeadapter(self.fn)
142
+ result = type_adapter.validate_python(arguments)
143
+ if inspect.isawaitable(result):
144
+ result = await result
145
+
146
+ return _convert_to_content(result, serializer=self.serializer)
147
147
 
148
148
  def to_mcp_tool(self, **overrides: Any) -> MCPTool:
149
149
  kwargs = {
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
7
7
 
8
- from fastmcp.exceptions import NotFoundError
8
+ from fastmcp.exceptions import NotFoundError, ToolError
9
9
  from fastmcp.settings import DuplicateBehavior
10
10
  from fastmcp.tools.tool import Tool
11
11
  from fastmcp.utilities.logging import get_logger
@@ -94,6 +94,20 @@ class ToolManager:
94
94
  self._tools[key] = tool
95
95
  return tool
96
96
 
97
+ def remove_tool(self, key: str) -> None:
98
+ """Remove a tool from the server.
99
+
100
+ Args:
101
+ key: The key of the tool to remove
102
+
103
+ Raises:
104
+ NotFoundError: If the tool is not found
105
+ """
106
+ if key in self._tools:
107
+ del self._tools[key]
108
+ else:
109
+ raise NotFoundError(f"Unknown tool: {key}")
110
+
97
111
  async def call_tool(
98
112
  self, key: str, arguments: dict[str, Any]
99
113
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
@@ -102,4 +116,15 @@ class ToolManager:
102
116
  if not tool:
103
117
  raise NotFoundError(f"Unknown tool: {key}")
104
118
 
105
- return await tool.run(arguments)
119
+ try:
120
+ return await tool.run(arguments)
121
+
122
+ # raise ToolErrors as-is
123
+ except ToolError as e:
124
+ logger.exception(f"Error calling tool {key!r}: {e}")
125
+ raise e
126
+
127
+ # raise other exceptions as ToolErrors without revealing internal details
128
+ except Exception as e:
129
+ logger.exception(f"Error calling tool {key!r}: {e}")
130
+ raise ToolError(f"Error calling tool {key!r}") from e
@@ -0,0 +1,49 @@
1
+ from collections.abc import Callable, Iterable, Mapping
2
+ from typing import Any
3
+
4
+ import httpx
5
+ import mcp.types
6
+ from exceptiongroup import BaseExceptionGroup
7
+ from mcp import McpError
8
+
9
+ import fastmcp
10
+
11
+
12
+ def iter_exc(group: BaseExceptionGroup):
13
+ for exc in group.exceptions:
14
+ if isinstance(exc, BaseExceptionGroup):
15
+ yield from iter_exc(exc)
16
+ else:
17
+ yield exc
18
+
19
+
20
+ def _exception_handler(group: BaseExceptionGroup):
21
+ for leaf in iter_exc(group):
22
+ if isinstance(leaf, httpx.ConnectTimeout):
23
+ raise McpError(
24
+ error=mcp.types.ErrorData(
25
+ code=httpx.codes.REQUEST_TIMEOUT,
26
+ message="Timed out while waiting for response.",
27
+ )
28
+ )
29
+ raise leaf
30
+
31
+
32
+ # this catch handler is used to catch taskgroup exception groups and raise the
33
+ # first exception. This allows more sane debugging.
34
+ _catch_handlers: Mapping[
35
+ type[BaseException] | Iterable[type[BaseException]],
36
+ Callable[[BaseExceptionGroup[Any]], Any],
37
+ ] = {
38
+ Exception: _exception_handler,
39
+ }
40
+
41
+
42
+ def get_catch_handlers() -> Mapping[
43
+ type[BaseException] | Iterable[type[BaseException]],
44
+ Callable[[BaseExceptionGroup[Any]], Any],
45
+ ]:
46
+ if fastmcp.settings.settings.client_raise_first_exceptiongroup_error:
47
+ return _catch_handlers
48
+ else:
49
+ return {}