fastmcp 2.3.3__py3-none-any.whl → 2.3.5__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/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 (
@@ -12,6 +11,7 @@ from contextlib import (
12
11
  asynccontextmanager,
13
12
  )
14
13
  from functools import partial
14
+ from pathlib import Path
15
15
  from typing import TYPE_CHECKING, Any, Generic, Literal
16
16
 
17
17
  import anyio
@@ -20,7 +20,7 @@ import pydantic
20
20
  import uvicorn
21
21
  from mcp.server.auth.provider import OAuthAuthorizationServerProvider
22
22
  from mcp.server.lowlevel.helper_types import ReadResourceContents
23
- from mcp.server.lowlevel.server import LifespanResultT
23
+ from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
24
24
  from mcp.server.lowlevel.server import Server as MCPServer
25
25
  from mcp.server.stdio import stdio_server
26
26
  from mcp.types import (
@@ -36,7 +36,6 @@ from mcp.types import Resource as MCPResource
36
36
  from mcp.types import ResourceTemplate as MCPResourceTemplate
37
37
  from mcp.types import Tool as MCPTool
38
38
  from pydantic import AnyUrl
39
- from starlette.applications import Starlette
40
39
  from starlette.middleware import Middleware
41
40
  from starlette.requests import Request
42
41
  from starlette.responses import Response
@@ -44,28 +43,34 @@ 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
51
50
  from fastmcp.resources.template import ResourceTemplate
52
- from fastmcp.server.http import create_sse_app
51
+ from fastmcp.server.http import (
52
+ StarletteWithLifespan,
53
+ create_sse_app,
54
+ create_streamable_http_app,
55
+ )
53
56
  from fastmcp.tools import ToolManager
54
57
  from fastmcp.tools.tool import Tool
55
58
  from fastmcp.utilities.cache import TimedCache
56
59
  from fastmcp.utilities.decorators import DecoratedFunction
57
- from fastmcp.utilities.logging import configure_logging, get_logger
60
+ from fastmcp.utilities.logging import get_logger
58
61
 
59
62
  if TYPE_CHECKING:
60
63
  from fastmcp.client import Client
64
+ from fastmcp.client.transports import ClientTransport
61
65
  from fastmcp.server.openapi import FastMCPOpenAPI
62
66
  from fastmcp.server.proxy import FastMCPProxy
63
-
64
67
  logger = get_logger(__name__)
65
68
 
69
+ DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
70
+
66
71
 
67
72
  @asynccontextmanager
68
- async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
73
+ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
69
74
  """Default lifespan context manager that does nothing.
70
75
 
71
76
  Args:
@@ -78,8 +83,10 @@ async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
78
83
 
79
84
 
80
85
  def _lifespan_wrapper(
81
- app: FastMCP,
82
- lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
86
+ app: FastMCP[LifespanResultT],
87
+ lifespan: Callable[
88
+ [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
89
+ ],
83
90
  ) -> Callable[
84
91
  [MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
85
92
  ]:
@@ -107,40 +114,52 @@ class FastMCP(Generic[LifespanResultT]):
107
114
  | None
108
115
  ) = None,
109
116
  tags: set[str] | None = None,
117
+ dependencies: list[str] | None = None,
110
118
  tool_serializer: Callable[[Any], str] | None = None,
119
+ cache_expiration_seconds: float | None = None,
120
+ on_duplicate_tools: DuplicateBehavior | None = None,
121
+ on_duplicate_resources: DuplicateBehavior | None = None,
122
+ on_duplicate_prompts: DuplicateBehavior | None = None,
111
123
  **settings: Any,
112
124
  ):
113
- self.tags: set[str] = tags or set()
125
+ if settings:
126
+ # TODO: remove settings. Deprecated since 2.3.4
127
+ warnings.warn(
128
+ "Passing runtime and transport-specific settings as kwargs "
129
+ "to the FastMCP constructor is deprecated (as of 2.3.4), "
130
+ "including most transport settings. If possible, provide settings when calling "
131
+ "run() instead.",
132
+ DeprecationWarning,
133
+ stacklevel=2,
134
+ )
114
135
  self.settings = fastmcp.settings.ServerSettings(**settings)
136
+
137
+ self.tags: set[str] = tags or set()
138
+ self.dependencies = dependencies
115
139
  self._cache = TimedCache(
116
- expiration=datetime.timedelta(
117
- seconds=self.settings.cache_expiration_seconds
118
- )
140
+ expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
119
141
  )
120
-
121
142
  self._mounted_servers: dict[str, MountedServer] = {}
143
+ self._additional_http_routes: list[BaseRoute] = []
144
+ self._tool_manager = ToolManager(
145
+ duplicate_behavior=on_duplicate_tools,
146
+ serializer=tool_serializer,
147
+ )
148
+ self._resource_manager = ResourceManager(
149
+ duplicate_behavior=on_duplicate_resources
150
+ )
151
+ self._prompt_manager = PromptManager(duplicate_behavior=on_duplicate_prompts)
122
152
 
123
153
  if lifespan is None:
124
154
  self._has_lifespan = False
125
155
  lifespan = default_lifespan
126
156
  else:
127
157
  self._has_lifespan = True
128
-
129
158
  self._mcp_server = MCPServer[LifespanResultT](
130
159
  name=name or "FastMCP",
131
160
  instructions=instructions,
132
161
  lifespan=_lifespan_wrapper(self, lifespan),
133
162
  )
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
163
 
145
164
  if (self.settings.auth is not None) != (auth_server_provider is not None):
146
165
  # TODO: after we support separate authorization servers (see
@@ -150,15 +169,9 @@ class FastMCP(Generic[LifespanResultT]):
150
169
  )
151
170
  self._auth_server_provider = auth_server_provider
152
171
 
153
- self._additional_http_routes: list[BaseRoute] = []
154
- self.dependencies = self.settings.dependencies
155
-
156
172
  # Set up MCP protocol handlers
157
173
  self._setup_handlers()
158
174
 
159
- # Configure logging
160
- configure_logging(self.settings.log_level)
161
-
162
175
  def __repr__(self) -> str:
163
176
  return f"{type(self).__name__}({self.name!r})"
164
177
 
@@ -182,15 +195,13 @@ class FastMCP(Generic[LifespanResultT]):
182
195
  """
183
196
  if transport is None:
184
197
  transport = "stdio"
185
- if transport not in ["stdio", "streamable-http", "sse"]:
198
+ if transport not in {"stdio", "streamable-http", "sse"}:
186
199
  raise ValueError(f"Unknown transport: {transport}")
187
200
 
188
201
  if transport == "stdio":
189
202
  await self.run_stdio_async(**transport_kwargs)
190
- elif transport == "streamable-http":
191
- await self.run_http_async(transport="streamable-http", **transport_kwargs)
192
- elif transport == "sse":
193
- await self.run_http_async(transport="sse", **transport_kwargs)
203
+ elif transport in {"streamable-http", "sse"}:
204
+ await self.run_http_async(transport=transport, **transport_kwargs)
194
205
  else:
195
206
  raise ValueError(f"Unknown transport: {transport}")
196
207
 
@@ -204,7 +215,6 @@ class FastMCP(Generic[LifespanResultT]):
204
215
  Args:
205
216
  transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
206
217
  """
207
- logger.info(f'Starting server "{self.name}"...')
208
218
 
209
219
  anyio.run(partial(self.run_async, transport, **transport_kwargs))
210
220
 
@@ -221,7 +231,7 @@ class FastMCP(Generic[LifespanResultT]):
221
231
  async def get_tools(self) -> dict[str, Tool]:
222
232
  """Get all registered tools, indexed by registered key."""
223
233
  if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
224
- tools = {}
234
+ tools: dict[str, Tool] = {}
225
235
  for server in self._mounted_servers.values():
226
236
  server_tools = await server.get_tools()
227
237
  tools.update(server_tools)
@@ -232,7 +242,7 @@ class FastMCP(Generic[LifespanResultT]):
232
242
  async def get_resources(self) -> dict[str, Resource]:
233
243
  """Get all registered resources, indexed by registered key."""
234
244
  if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
235
- resources = {}
245
+ resources: dict[str, Resource] = {}
236
246
  for server in self._mounted_servers.values():
237
247
  server_resources = await server.get_resources()
238
248
  resources.update(server_resources)
@@ -245,7 +255,7 @@ class FastMCP(Generic[LifespanResultT]):
245
255
  if (
246
256
  templates := self._cache.get("resource_templates")
247
257
  ) is self._cache.NOT_FOUND:
248
- templates = {}
258
+ templates: dict[str, ResourceTemplate] = {}
249
259
  for server in self._mounted_servers.values():
250
260
  server_templates = await server.get_resource_templates()
251
261
  templates.update(server_templates)
@@ -258,7 +268,7 @@ class FastMCP(Generic[LifespanResultT]):
258
268
  List all available prompts.
259
269
  """
260
270
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
261
- prompts = {}
271
+ prompts: dict[str, Prompt] = {}
262
272
  for server in self._mounted_servers.values():
263
273
  server_prompts = await server.get_prompts()
264
274
  prompts.update(server_prompts)
@@ -378,16 +388,13 @@ class FastMCP(Generic[LifespanResultT]):
378
388
  with fastmcp.server.context.Context(fastmcp=self):
379
389
  if self._resource_manager.has_resource(uri):
380
390
  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))
391
+ content = await self._resource_manager.read_resource(uri)
392
+ return [
393
+ ReadResourceContents(
394
+ content=content,
395
+ mime_type=resource.mime_type,
396
+ )
397
+ ]
391
398
  else:
392
399
  for server in self._mounted_servers.values():
393
400
  if server.match_resource(str(uri)):
@@ -414,7 +421,7 @@ class FastMCP(Generic[LifespanResultT]):
414
421
  for server in self._mounted_servers.values():
415
422
  if server.match_prompt(name):
416
423
  new_key = server.strip_prompt_prefix(name)
417
- return await server.server._mcp_get_prompt(new_key, arguments)
424
+ return await server.server._mcp_get_prompt(new_key, arguments)
418
425
  else:
419
426
  raise NotFoundError(f"Unknown prompt: {name}")
420
427
 
@@ -450,6 +457,18 @@ class FastMCP(Generic[LifespanResultT]):
450
457
  )
451
458
  self._cache.clear()
452
459
 
460
+ def remove_tool(self, name: str) -> None:
461
+ """Remove a tool from the server.
462
+
463
+ Args:
464
+ name: The name of the tool to remove
465
+
466
+ Raises:
467
+ NotFoundError: If the tool is not found
468
+ """
469
+ self._tool_manager.remove_tool(name)
470
+ self._cache.clear()
471
+
453
472
  def tool(
454
473
  self,
455
474
  name: str | None = None,
@@ -712,10 +731,13 @@ class FastMCP(Generic[LifespanResultT]):
712
731
  async def run_stdio_async(self) -> None:
713
732
  """Run the server using stdio transport."""
714
733
  async with stdio_server() as (read_stream, write_stream):
734
+ logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
715
735
  await self._mcp_server.run(
716
736
  read_stream,
717
737
  write_stream,
718
- self._mcp_server.create_initialization_options(),
738
+ self._mcp_server.create_initialization_options(
739
+ NotificationOptions(tools_changed=True)
740
+ ),
719
741
  )
720
742
 
721
743
  async def run_http_async(
@@ -725,7 +747,8 @@ class FastMCP(Generic[LifespanResultT]):
725
747
  port: int | None = None,
726
748
  log_level: str | None = None,
727
749
  path: str | None = None,
728
- uvicorn_config: dict | None = None,
750
+ uvicorn_config: dict[str, Any] | None = None,
751
+ middleware: list[Middleware] | None = None,
729
752
  ) -> None:
730
753
  """Run the server using HTTP transport.
731
754
 
@@ -737,21 +760,29 @@ class FastMCP(Generic[LifespanResultT]):
737
760
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
738
761
  uvicorn_config: Additional configuration for the Uvicorn server
739
762
  """
740
- uvicorn_config = uvicorn_config or {}
741
- uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
742
- # lifespan is required for streamable http
743
- uvicorn_config["lifespan"] = "on"
744
-
745
- app = self.http_app(path=path, transport=transport)
746
-
747
- config = uvicorn.Config(
748
- app,
749
- host=host or self.settings.host,
750
- port=port or self.settings.port,
751
- log_level=log_level or self.settings.log_level.lower(),
752
- **uvicorn_config,
753
- )
763
+ host = host or self.settings.host
764
+ port = port or self.settings.port
765
+ default_log_level_to_use = log_level or self.settings.log_level.lower()
766
+
767
+ app = self.http_app(path=path, transport=transport, middleware=middleware)
768
+
769
+ _uvicorn_config_from_user = uvicorn_config or {}
770
+
771
+ config_kwargs: dict[str, Any] = {
772
+ "timeout_graceful_shutdown": 0,
773
+ "lifespan": "on",
774
+ }
775
+ config_kwargs.update(_uvicorn_config_from_user)
776
+
777
+ if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
778
+ config_kwargs["log_level"] = default_log_level_to_use
779
+
780
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
754
781
  server = uvicorn.Server(config)
782
+ path = app.state.path.lstrip("/") # type: ignore
783
+ logger.info(
784
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
785
+ )
755
786
  await server.serve()
756
787
 
757
788
  async def run_sse_async(
@@ -761,18 +792,17 @@ class FastMCP(Generic[LifespanResultT]):
761
792
  log_level: str | None = None,
762
793
  path: str | None = None,
763
794
  message_path: str | None = None,
764
- uvicorn_config: dict | None = None,
795
+ uvicorn_config: dict[str, Any] | None = None,
765
796
  ) -> None:
766
797
  """Run the server using SSE transport."""
798
+
799
+ # Deprecated since 2.3.2
767
800
  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
- ),
801
+ "The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
802
+ "modern (non-SSE) alternative, or create an SSE app with "
803
+ "`fastmcp.server.http.create_sse_app` and run it directly.",
775
804
  DeprecationWarning,
805
+ stacklevel=2,
776
806
  )
777
807
  await self.run_http_async(
778
808
  transport="sse",
@@ -788,7 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
788
818
  path: str | None = None,
789
819
  message_path: str | None = None,
790
820
  middleware: list[Middleware] | None = None,
791
- ) -> Starlette:
821
+ ) -> StarletteWithLifespan:
792
822
  """
793
823
  Create a Starlette app for the SSE server.
794
824
 
@@ -797,14 +827,12 @@ class FastMCP(Generic[LifespanResultT]):
797
827
  message_path: The path to the message endpoint
798
828
  middleware: A list of middleware to apply to the app
799
829
  """
830
+ # Deprecated since 2.3.2
800
831
  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
- ),
832
+ "The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
833
+ "alternative, or call `fastmcp.server.http.create_sse_app` directly.",
807
834
  DeprecationWarning,
835
+ stacklevel=2,
808
836
  )
809
837
  return create_sse_app(
810
838
  server=self,
@@ -821,7 +849,7 @@ class FastMCP(Generic[LifespanResultT]):
821
849
  self,
822
850
  path: str | None = None,
823
851
  middleware: list[Middleware] | None = None,
824
- ) -> Starlette:
852
+ ) -> StarletteWithLifespan:
825
853
  """
826
854
  Create a Starlette app for the StreamableHTTP server.
827
855
 
@@ -829,9 +857,11 @@ class FastMCP(Generic[LifespanResultT]):
829
857
  path: The path to the StreamableHTTP endpoint
830
858
  middleware: A list of middleware to apply to the app
831
859
  """
860
+ # Deprecated since 2.3.2
832
861
  warnings.warn(
833
- "The streamable_http_app method is deprecated. Use http_app() instead.",
862
+ "The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
834
863
  DeprecationWarning,
864
+ stacklevel=2,
835
865
  )
836
866
  return self.http_app(path=path, middleware=middleware)
837
867
 
@@ -840,7 +870,7 @@ class FastMCP(Generic[LifespanResultT]):
840
870
  path: str | None = None,
841
871
  middleware: list[Middleware] | None = None,
842
872
  transport: Literal["streamable-http", "sse"] = "streamable-http",
843
- ) -> Starlette:
873
+ ) -> StarletteWithLifespan:
844
874
  """Create a Starlette app using the specified HTTP transport.
845
875
 
846
876
  Args:
@@ -851,7 +881,6 @@ class FastMCP(Generic[LifespanResultT]):
851
881
  Returns:
852
882
  A Starlette application configured with the specified transport
853
883
  """
854
- from fastmcp.server.http import create_streamable_http_app
855
884
 
856
885
  if transport == "streamable-http":
857
886
  return create_streamable_http_app(
@@ -884,11 +913,14 @@ class FastMCP(Generic[LifespanResultT]):
884
913
  port: int | None = None,
885
914
  log_level: str | None = None,
886
915
  path: str | None = None,
887
- uvicorn_config: dict | None = None,
916
+ uvicorn_config: dict[str, Any] | None = None,
888
917
  ) -> None:
918
+ # Deprecated since 2.3.2
889
919
  warnings.warn(
890
- "The run_streamable_http_async method is deprecated. Use run_http_async instead.",
920
+ "The run_streamable_http_async method is deprecated (as of 2.3.2). "
921
+ "Use run_http_async instead.",
891
922
  DeprecationWarning,
923
+ stacklevel=2,
892
924
  )
893
925
  await self.run_http_async(
894
926
  transport="streamable-http",
@@ -1008,9 +1040,6 @@ class FastMCP(Generic[LifespanResultT]):
1008
1040
  - The prompts are imported with prefixed names using the
1009
1041
  prompt_separator Example: If server has a prompt named
1010
1042
  "weather_prompt", it will be available as "weather_weather_prompt"
1011
- - The mounted server's lifespan will be executed when the parent
1012
- server's lifespan runs, ensuring that any setup needed by the mounted
1013
- server is performed
1014
1043
 
1015
1044
  Args:
1016
1045
  prefix: The prefix to use for the mounted server server: The FastMCP
@@ -1082,14 +1111,48 @@ class FastMCP(Generic[LifespanResultT]):
1082
1111
  openapi_spec=app.openapi(), client=client, name=name, **settings
1083
1112
  )
1084
1113
 
1114
+ @classmethod
1115
+ def as_proxy(
1116
+ cls,
1117
+ backend: Client
1118
+ | ClientTransport
1119
+ | FastMCP[Any]
1120
+ | AnyUrl
1121
+ | Path
1122
+ | dict[str, Any]
1123
+ | str,
1124
+ **settings: Any,
1125
+ ) -> FastMCPProxy:
1126
+ """Create a FastMCP proxy server for the given backend.
1127
+
1128
+ The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client`
1129
+ instance or any value accepted as the ``transport`` argument of
1130
+ :class:`~fastmcp.client.Client`. This mirrors the convenience of the
1131
+ ``Client`` constructor.
1132
+ """
1133
+ from fastmcp.client.client import Client
1134
+ from fastmcp.server.proxy import FastMCPProxy
1135
+
1136
+ if isinstance(backend, Client):
1137
+ client = backend
1138
+ else:
1139
+ client = Client(backend)
1140
+
1141
+ return FastMCPProxy(client=client, **settings)
1142
+
1085
1143
  @classmethod
1086
1144
  def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
1087
1145
  """
1088
1146
  Create a FastMCP proxy server from a FastMCP client.
1089
1147
  """
1090
- from fastmcp.server.proxy import FastMCPProxy
1148
+ # Deprecated since 2.3.5
1149
+ warnings.warn(
1150
+ "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
1151
+ DeprecationWarning,
1152
+ stacklevel=2,
1153
+ )
1091
1154
 
1092
- return FastMCPProxy(client=client, **settings)
1155
+ return cls.as_proxy(client, **settings)
1093
1156
 
1094
1157
 
1095
1158
  def _validate_resource_prefix(prefix: str) -> None:
@@ -1108,7 +1171,7 @@ class MountedServer:
1108
1171
  def __init__(
1109
1172
  self,
1110
1173
  prefix: str,
1111
- server: FastMCP,
1174
+ server: FastMCP[LifespanResultT],
1112
1175
  tool_separator: str | None = None,
1113
1176
  resource_separator: str | None = None,
1114
1177
  prompt_separator: str | None = None,
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()