fastmcp 2.13.0rc2__py3-none-any.whl → 2.13.0.1__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 (81) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +3 -2
  3. fastmcp/cli/install/claude_code.py +3 -3
  4. fastmcp/client/__init__.py +9 -9
  5. fastmcp/client/auth/oauth.py +7 -6
  6. fastmcp/client/client.py +10 -10
  7. fastmcp/client/oauth_callback.py +6 -2
  8. fastmcp/client/sampling.py +1 -1
  9. fastmcp/client/transports.py +35 -34
  10. fastmcp/contrib/component_manager/__init__.py +1 -1
  11. fastmcp/contrib/component_manager/component_manager.py +2 -2
  12. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  13. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  14. fastmcp/experimental/server/openapi/__init__.py +5 -8
  15. fastmcp/experimental/server/openapi/components.py +11 -7
  16. fastmcp/experimental/server/openapi/routing.py +2 -2
  17. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  18. fastmcp/experimental/utilities/openapi/director.py +1 -1
  19. fastmcp/experimental/utilities/openapi/json_schema_converter.py +2 -2
  20. fastmcp/experimental/utilities/openapi/models.py +3 -3
  21. fastmcp/experimental/utilities/openapi/parser.py +3 -5
  22. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  23. fastmcp/mcp_config.py +2 -3
  24. fastmcp/prompts/__init__.py +1 -1
  25. fastmcp/prompts/prompt.py +9 -13
  26. fastmcp/resources/__init__.py +5 -5
  27. fastmcp/resources/resource.py +1 -3
  28. fastmcp/resources/resource_manager.py +1 -1
  29. fastmcp/resources/types.py +30 -24
  30. fastmcp/server/__init__.py +1 -1
  31. fastmcp/server/auth/__init__.py +5 -5
  32. fastmcp/server/auth/auth.py +2 -2
  33. fastmcp/server/auth/handlers/authorize.py +324 -0
  34. fastmcp/server/auth/jwt_issuer.py +39 -92
  35. fastmcp/server/auth/middleware.py +96 -0
  36. fastmcp/server/auth/oauth_proxy.py +236 -217
  37. fastmcp/server/auth/oidc_proxy.py +18 -3
  38. fastmcp/server/auth/providers/auth0.py +28 -15
  39. fastmcp/server/auth/providers/aws.py +16 -1
  40. fastmcp/server/auth/providers/azure.py +101 -40
  41. fastmcp/server/auth/providers/bearer.py +1 -1
  42. fastmcp/server/auth/providers/github.py +16 -1
  43. fastmcp/server/auth/providers/google.py +16 -1
  44. fastmcp/server/auth/providers/in_memory.py +2 -2
  45. fastmcp/server/auth/providers/introspection.py +2 -2
  46. fastmcp/server/auth/providers/jwt.py +17 -18
  47. fastmcp/server/auth/providers/supabase.py +1 -1
  48. fastmcp/server/auth/providers/workos.py +18 -3
  49. fastmcp/server/context.py +41 -12
  50. fastmcp/server/dependencies.py +5 -6
  51. fastmcp/server/elicitation.py +1 -1
  52. fastmcp/server/http.py +3 -4
  53. fastmcp/server/middleware/__init__.py +1 -1
  54. fastmcp/server/middleware/caching.py +1 -1
  55. fastmcp/server/middleware/error_handling.py +8 -8
  56. fastmcp/server/middleware/middleware.py +1 -1
  57. fastmcp/server/middleware/tool_injection.py +116 -0
  58. fastmcp/server/openapi.py +10 -6
  59. fastmcp/server/proxy.py +5 -4
  60. fastmcp/server/server.py +74 -55
  61. fastmcp/settings.py +2 -1
  62. fastmcp/tools/__init__.py +1 -1
  63. fastmcp/tools/tool.py +12 -12
  64. fastmcp/tools/tool_manager.py +8 -4
  65. fastmcp/tools/tool_transform.py +6 -6
  66. fastmcp/utilities/cli.py +50 -21
  67. fastmcp/utilities/inspect.py +2 -2
  68. fastmcp/utilities/json_schema_type.py +4 -4
  69. fastmcp/utilities/logging.py +14 -18
  70. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  71. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  72. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  73. fastmcp/utilities/openapi.py +9 -9
  74. fastmcp/utilities/tests.py +2 -4
  75. fastmcp/utilities/ui.py +126 -6
  76. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/METADATA +5 -5
  77. fastmcp-2.13.0.1.dist-info/RECORD +141 -0
  78. fastmcp-2.13.0rc2.dist-info/RECORD +0 -138
  79. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/WHEEL +0 -0
  80. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/entry_points.txt +0 -0
  81. {fastmcp-2.13.0rc2.dist-info → fastmcp-2.13.0.1.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -7,8 +7,19 @@ import json
7
7
  import re
8
8
  import secrets
9
9
  import warnings
10
- from collections.abc import AsyncIterator, Awaitable, Callable
11
- from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
10
+ from collections.abc import (
11
+ AsyncIterator,
12
+ Awaitable,
13
+ Callable,
14
+ Collection,
15
+ Mapping,
16
+ Sequence,
17
+ )
18
+ from contextlib import (
19
+ AbstractAsyncContextManager,
20
+ AsyncExitStack,
21
+ asynccontextmanager,
22
+ )
12
23
  from dataclasses import dataclass
13
24
  from functools import partial
14
25
  from pathlib import Path
@@ -43,9 +54,11 @@ import fastmcp
43
54
  import fastmcp.server
44
55
  from fastmcp.exceptions import DisabledError, NotFoundError
45
56
  from fastmcp.mcp_config import MCPConfig
46
- from fastmcp.prompts import Prompt, PromptManager
57
+ from fastmcp.prompts import Prompt
47
58
  from fastmcp.prompts.prompt import FunctionPrompt
48
- from fastmcp.resources import Resource, ResourceManager
59
+ from fastmcp.prompts.prompt_manager import PromptManager
60
+ from fastmcp.resources.resource import Resource
61
+ from fastmcp.resources.resource_manager import ResourceManager
49
62
  from fastmcp.resources.template import ResourceTemplate
50
63
  from fastmcp.server.auth import AuthProvider
51
64
  from fastmcp.server.http import (
@@ -56,8 +69,8 @@ from fastmcp.server.http import (
56
69
  from fastmcp.server.low_level import LowLevelServer
57
70
  from fastmcp.server.middleware import Middleware, MiddlewareContext
58
71
  from fastmcp.settings import Settings
59
- from fastmcp.tools import ToolManager
60
72
  from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
73
+ from fastmcp.tools.tool_manager import ToolManager
61
74
  from fastmcp.tools.tool_transform import ToolTransformConfig
62
75
  from fastmcp.utilities.cli import log_server_banner
63
76
  from fastmcp.utilities.components import FastMCPComponent
@@ -66,7 +79,6 @@ from fastmcp.utilities.types import NotSet, NotSetT
66
79
 
67
80
  if TYPE_CHECKING:
68
81
  from fastmcp.client import Client
69
- from fastmcp.client.sampling import ServerSamplingHandler
70
82
  from fastmcp.client.transports import ClientTransport, ClientTransportT
71
83
  from fastmcp.experimental.server.openapi import FastMCPOpenAPI as FastMCPOpenAPINew
72
84
  from fastmcp.experimental.server.openapi.routing import (
@@ -80,6 +92,8 @@ if TYPE_CHECKING:
80
92
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
81
93
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
82
94
  from fastmcp.server.proxy import FastMCPProxy
95
+ from fastmcp.server.sampling.handler import ServerSamplingHandler
96
+ from fastmcp.tools.tool import ToolResultSerializerType
83
97
 
84
98
  logger = get_logger(__name__)
85
99
 
@@ -140,17 +154,17 @@ class FastMCP(Generic[LifespanResultT]):
140
154
  version: str | None = None,
141
155
  website_url: str | None = None,
142
156
  icons: list[mcp.types.Icon] | None = None,
143
- auth: AuthProvider | None | NotSetT = NotSet,
144
- middleware: list[Middleware] | None = None,
157
+ auth: AuthProvider | NotSetT | None = NotSet,
158
+ middleware: Sequence[Middleware] | None = None,
145
159
  lifespan: LifespanCallable | None = None,
146
160
  dependencies: list[str] | None = None,
147
161
  resource_prefix_format: Literal["protocol", "path"] | None = None,
148
162
  mask_error_details: bool | None = None,
149
- tools: list[Tool | Callable[..., Any]] | None = None,
150
- tool_transformations: dict[str, ToolTransformConfig] | None = None,
151
- tool_serializer: Callable[[Any], str] | None = None,
152
- include_tags: set[str] | None = None,
153
- exclude_tags: set[str] | None = None,
163
+ tools: Sequence[Tool | Callable[..., Any]] | None = None,
164
+ tool_transformations: Mapping[str, ToolTransformConfig] | None = None,
165
+ tool_serializer: ToolResultSerializerType | None = None,
166
+ include_tags: Collection[str] | None = None,
167
+ exclude_tags: Collection[str] | None = None,
154
168
  include_fastmcp_meta: bool | None = None,
155
169
  on_duplicate_tools: DuplicateBehavior | None = None,
156
170
  on_duplicate_resources: DuplicateBehavior | None = None,
@@ -179,27 +193,29 @@ class FastMCP(Generic[LifespanResultT]):
179
193
 
180
194
  self._additional_http_routes: list[BaseRoute] = []
181
195
  self._mounted_servers: list[MountedServer] = []
182
- self._tool_manager = ToolManager(
196
+ self._tool_manager: ToolManager = ToolManager(
183
197
  duplicate_behavior=on_duplicate_tools,
184
198
  mask_error_details=mask_error_details,
185
199
  transformations=tool_transformations,
186
200
  )
187
- self._resource_manager = ResourceManager(
201
+ self._resource_manager: ResourceManager = ResourceManager(
188
202
  duplicate_behavior=on_duplicate_resources,
189
203
  mask_error_details=mask_error_details,
190
204
  )
191
- self._prompt_manager = PromptManager(
205
+ self._prompt_manager: PromptManager = PromptManager(
192
206
  duplicate_behavior=on_duplicate_prompts,
193
207
  mask_error_details=mask_error_details,
194
208
  )
195
- self._tool_serializer = tool_serializer
209
+ self._tool_serializer: Callable[[Any], str] | None = tool_serializer
196
210
 
197
211
  self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
198
212
  self._lifespan_result: LifespanResultT | None = None
199
- self._lifespan_result_set = False
213
+ self._lifespan_result_set: bool = False
200
214
 
201
215
  # Generate random ID if no name provided
202
- self._mcp_server = LowLevelServer[LifespanResultT](
216
+ self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
217
+ LifespanResultT
218
+ ](
203
219
  fastmcp=self,
204
220
  name=name or self.generate_name(),
205
221
  version=version or fastmcp.__version__,
@@ -216,7 +232,7 @@ class FastMCP(Generic[LifespanResultT]):
216
232
  auth = fastmcp.settings.server_auth_class()
217
233
  else:
218
234
  auth = None
219
- self.auth = cast(AuthProvider | None, auth)
235
+ self.auth: AuthProvider | None = cast(AuthProvider | None, auth)
220
236
 
221
237
  if tools:
222
238
  for tool in tools:
@@ -224,15 +240,20 @@ class FastMCP(Generic[LifespanResultT]):
224
240
  tool = Tool.from_function(tool, serializer=self._tool_serializer)
225
241
  self.add_tool(tool)
226
242
 
227
- self.include_tags = include_tags
228
- self.exclude_tags = exclude_tags
229
- self.strict_input_validation = (
243
+ self.include_tags: set[str] | None = (
244
+ set(include_tags) if include_tags is not None else None
245
+ )
246
+ self.exclude_tags: set[str] | None = (
247
+ set(exclude_tags) if exclude_tags is not None else None
248
+ )
249
+
250
+ self.strict_input_validation: bool = (
230
251
  strict_input_validation
231
252
  if strict_input_validation is not None
232
253
  else fastmcp.settings.strict_input_validation
233
254
  )
234
255
 
235
- self.middleware = middleware or []
256
+ self.middleware: list[Middleware] = list(middleware or [])
236
257
 
237
258
  # Set up MCP protocol handlers
238
259
  self._setup_handlers()
@@ -251,14 +272,18 @@ class FastMCP(Generic[LifespanResultT]):
251
272
  DeprecationWarning,
252
273
  stacklevel=2,
253
274
  )
254
- self.dependencies = (
275
+ self.dependencies: list[str] = (
255
276
  dependencies or fastmcp.settings.server_dependencies
256
277
  ) # TODO: Remove (deprecated in v2.11.4)
257
278
 
258
- self.sampling_handler = sampling_handler
259
- self.sampling_handler_behavior = sampling_handler_behavior or "fallback"
279
+ self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
280
+ sampling_handler
281
+ )
282
+ self.sampling_handler_behavior: Literal["always", "fallback"] = (
283
+ sampling_handler_behavior or "fallback"
284
+ )
260
285
 
261
- self.include_fastmcp_meta = (
286
+ self.include_fastmcp_meta: bool = (
262
287
  include_fastmcp_meta
263
288
  if include_fastmcp_meta is not None
264
289
  else fastmcp.settings.include_fastmcp_meta
@@ -1041,10 +1066,10 @@ class FastMCP(Generic[LifespanResultT]):
1041
1066
  try:
1042
1067
  result = await self._call_tool_middleware(key, arguments)
1043
1068
  return result.to_mcp_result()
1044
- except DisabledError:
1045
- raise NotFoundError(f"Unknown tool: {key}")
1046
- except NotFoundError:
1047
- raise NotFoundError(f"Unknown tool: {key}")
1069
+ except DisabledError as e:
1070
+ raise NotFoundError(f"Unknown tool: {key}") from e
1071
+ except NotFoundError as e:
1072
+ raise NotFoundError(f"Unknown tool: {key}") from e
1048
1073
 
1049
1074
  async def _call_tool_middleware(
1050
1075
  self,
@@ -1121,12 +1146,12 @@ class FastMCP(Generic[LifespanResultT]):
1121
1146
  return list[ReadResourceContents](
1122
1147
  await self._read_resource_middleware(uri)
1123
1148
  )
1124
- except DisabledError:
1149
+ except DisabledError as e:
1125
1150
  # convert to NotFoundError to avoid leaking resource presence
1126
- raise NotFoundError(f"Unknown resource: {str(uri)!r}")
1127
- except NotFoundError:
1151
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
1152
+ except NotFoundError as e:
1128
1153
  # standardize NotFound message
1129
- raise NotFoundError(f"Unknown resource: {str(uri)!r}")
1154
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
1130
1155
 
1131
1156
  async def _read_resource_middleware(
1132
1157
  self,
@@ -1137,10 +1162,7 @@ class FastMCP(Generic[LifespanResultT]):
1137
1162
  """
1138
1163
 
1139
1164
  # Convert string URI to AnyUrl if needed
1140
- if isinstance(uri, str):
1141
- uri_param = AnyUrl(uri)
1142
- else:
1143
- uri_param = uri
1165
+ uri_param = AnyUrl(uri) if isinstance(uri, str) else uri
1144
1166
 
1145
1167
  mw_context = MiddlewareContext(
1146
1168
  message=mcp.types.ReadResourceRequestParams(uri=uri_param),
@@ -1220,12 +1242,12 @@ class FastMCP(Generic[LifespanResultT]):
1220
1242
  async with fastmcp.server.context.Context(fastmcp=self):
1221
1243
  try:
1222
1244
  return await self._get_prompt_middleware(name, arguments)
1223
- except DisabledError:
1245
+ except DisabledError as e:
1224
1246
  # convert to NotFoundError to avoid leaking prompt presence
1225
- raise NotFoundError(f"Unknown prompt: {name}")
1226
- except NotFoundError:
1247
+ raise NotFoundError(f"Unknown prompt: {name}") from e
1248
+ except NotFoundError as e:
1227
1249
  # standardize NotFound message
1228
- raise NotFoundError(f"Unknown prompt: {name}")
1250
+ raise NotFoundError(f"Unknown prompt: {name}") from e
1229
1251
 
1230
1252
  async def _get_prompt_middleware(
1231
1253
  self, name: str, arguments: dict[str, Any] | None = None
@@ -1348,7 +1370,7 @@ class FastMCP(Generic[LifespanResultT]):
1348
1370
  description: str | None = None,
1349
1371
  icons: list[mcp.types.Icon] | None = None,
1350
1372
  tags: set[str] | None = None,
1351
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1373
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
1352
1374
  annotations: ToolAnnotations | dict[str, Any] | None = None,
1353
1375
  exclude_args: list[str] | None = None,
1354
1376
  meta: dict[str, Any] | None = None,
@@ -1365,7 +1387,7 @@ class FastMCP(Generic[LifespanResultT]):
1365
1387
  description: str | None = None,
1366
1388
  icons: list[mcp.types.Icon] | None = None,
1367
1389
  tags: set[str] | None = None,
1368
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1390
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
1369
1391
  annotations: ToolAnnotations | dict[str, Any] | None = None,
1370
1392
  exclude_args: list[str] | None = None,
1371
1393
  meta: dict[str, Any] | None = None,
@@ -1381,7 +1403,7 @@ class FastMCP(Generic[LifespanResultT]):
1381
1403
  description: str | None = None,
1382
1404
  icons: list[mcp.types.Icon] | None = None,
1383
1405
  tags: set[str] | None = None,
1384
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
1406
+ output_schema: dict[str, Any] | NotSetT | None = NotSet,
1385
1407
  annotations: ToolAnnotations | dict[str, Any] | None = None,
1386
1408
  exclude_args: list[str] | None = None,
1387
1409
  meta: dict[str, Any] | None = None,
@@ -2008,14 +2030,14 @@ class FastMCP(Generic[LifespanResultT]):
2008
2030
  port=port,
2009
2031
  path=server_path,
2010
2032
  )
2011
- _uvicorn_config_from_user = uvicorn_config or {}
2033
+ uvicorn_config_from_user = uvicorn_config or {}
2012
2034
 
2013
2035
  config_kwargs: dict[str, Any] = {
2014
2036
  "timeout_graceful_shutdown": 0,
2015
2037
  "lifespan": "on",
2016
2038
  "ws": "websockets-sansio",
2017
2039
  }
2018
- config_kwargs.update(_uvicorn_config_from_user)
2040
+ config_kwargs.update(uvicorn_config_from_user)
2019
2041
 
2020
2042
  if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
2021
2043
  config_kwargs["log_level"] = default_log_level_to_use
@@ -2584,8 +2606,8 @@ class FastMCP(Generic[LifespanResultT]):
2584
2606
  # - Connected clients: reuse existing session for all requests
2585
2607
  # - Disconnected clients: create fresh sessions per request for isolation
2586
2608
  if client.is_connected():
2587
- _proxy_logger = get_logger(__name__)
2588
- _proxy_logger.info(
2609
+ proxy_logger = get_logger(__name__)
2610
+ proxy_logger.info(
2589
2611
  "Proxy detected connected client - reusing existing session for all requests. "
2590
2612
  "This may cause context mixing in concurrent scenarios."
2591
2613
  )
@@ -2657,10 +2679,7 @@ class FastMCP(Generic[LifespanResultT]):
2657
2679
  return False
2658
2680
 
2659
2681
  if self.include_tags is not None:
2660
- if any(itag in component.tags for itag in self.include_tags):
2661
- return True
2662
- else:
2663
- return False
2682
+ return bool(any(itag in component.tags for itag in self.include_tags))
2664
2683
 
2665
2684
  return True
2666
2685
 
fastmcp/settings.py CHANGED
@@ -6,6 +6,7 @@ import warnings
6
6
  from pathlib import Path
7
7
  from typing import TYPE_CHECKING, Annotated, Any, Literal
8
8
 
9
+ from platformdirs import user_data_dir
9
10
  from pydantic import Field, ImportString, field_validator
10
11
  from pydantic.fields import FieldInfo
11
12
  from pydantic_settings import (
@@ -150,7 +151,7 @@ class Settings(BaseSettings):
150
151
  )
151
152
  return self
152
153
 
153
- home: Path = Path.home() / ".fastmcp"
154
+ home: Path = Path(user_data_dir("fastmcp", appauthor=False))
154
155
 
155
156
  test_mode: bool = False
156
157
 
fastmcp/tools/__init__.py CHANGED
@@ -2,4 +2,4 @@ from .tool import Tool, FunctionTool
2
2
  from .tool_manager import ToolManager
3
3
  from .tool_transform import forward, forward_raw
4
4
 
5
- __all__ = ["Tool", "ToolManager", "FunctionTool", "forward", "forward_raw"]
5
+ __all__ = ["FunctionTool", "Tool", "ToolManager", "forward", "forward_raw"]
fastmcp/tools/tool.py CHANGED
@@ -173,7 +173,7 @@ class Tool(FastMCPComponent):
173
173
  tags: set[str] | None = None,
174
174
  annotations: ToolAnnotations | None = None,
175
175
  exclude_args: list[str] | None = None,
176
- output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
176
+ output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
177
177
  serializer: ToolResultSerializerType | None = None,
178
178
  meta: dict[str, Any] | None = None,
179
179
  enabled: bool | None = None,
@@ -212,13 +212,13 @@ class Tool(FastMCPComponent):
212
212
  tool: Tool,
213
213
  *,
214
214
  name: str | None = None,
215
- title: str | None | NotSetT = NotSet,
216
- description: str | None | NotSetT = NotSet,
215
+ title: str | NotSetT | None = NotSet,
216
+ description: str | NotSetT | None = NotSet,
217
217
  tags: set[str] | None = None,
218
- annotations: ToolAnnotations | None | NotSetT = NotSet,
219
- output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
218
+ annotations: ToolAnnotations | NotSetT | None = NotSet,
219
+ output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
220
220
  serializer: ToolResultSerializerType | None = None,
221
- meta: dict[str, Any] | None | NotSetT = NotSet,
221
+ meta: dict[str, Any] | NotSetT | None = NotSet,
222
222
  transform_args: dict[str, ArgTransform] | None = None,
223
223
  enabled: bool | None = None,
224
224
  transform_fn: Callable[..., Any] | None = None,
@@ -255,7 +255,7 @@ class FunctionTool(Tool):
255
255
  tags: set[str] | None = None,
256
256
  annotations: ToolAnnotations | None = None,
257
257
  exclude_args: list[str] | None = None,
258
- output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
258
+ output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
259
259
  serializer: ToolResultSerializerType | None = None,
260
260
  meta: dict[str, Any] | None = None,
261
261
  enabled: bool | None = None,
@@ -446,9 +446,8 @@ class ParsedFunction:
446
446
  # we ensure that no output schema is automatically generated.
447
447
  clean_output_type = replace_type(
448
448
  output_type,
449
- {
450
- t: _UnserializableType
451
- for t in (
449
+ dict.fromkeys( # type: ignore[arg-type]
450
+ (
452
451
  Image,
453
452
  Audio,
454
453
  File,
@@ -458,8 +457,9 @@ class ParsedFunction:
458
457
  mcp.types.AudioContent,
459
458
  mcp.types.ResourceLink,
460
459
  mcp.types.EmbeddedResource,
461
- )
462
- },
460
+ ),
461
+ _UnserializableType,
462
+ ),
463
463
  )
464
464
 
465
465
  try:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
- from collections.abc import Callable
4
+ from collections.abc import Callable, Mapping
5
5
  from typing import Any
6
6
 
7
7
  from mcp.types import ToolAnnotations
@@ -27,11 +27,15 @@ class ToolManager:
27
27
  self,
28
28
  duplicate_behavior: DuplicateBehavior | None = None,
29
29
  mask_error_details: bool | None = None,
30
- transformations: dict[str, ToolTransformConfig] | None = None,
30
+ transformations: Mapping[str, ToolTransformConfig] | None = None,
31
31
  ):
32
32
  self._tools: dict[str, Tool] = {}
33
- self.mask_error_details = mask_error_details or settings.mask_error_details
34
- self.transformations = transformations or {}
33
+ self.mask_error_details: bool = (
34
+ mask_error_details or settings.mask_error_details
35
+ )
36
+ self.transformations: dict[str, ToolTransformConfig] = dict(
37
+ transformations or {}
38
+ )
35
39
 
36
40
  # Default to "warn" if None is provided
37
41
  if duplicate_behavior is None:
@@ -365,15 +365,15 @@ class TransformedTool(Tool):
365
365
  cls,
366
366
  tool: Tool,
367
367
  name: str | None = None,
368
- title: str | None | NotSetT = NotSet,
369
- description: str | None | NotSetT = NotSet,
368
+ title: str | NotSetT | None = NotSet,
369
+ description: str | NotSetT | None = NotSet,
370
370
  tags: set[str] | None = None,
371
371
  transform_fn: Callable[..., Any] | None = None,
372
372
  transform_args: dict[str, ArgTransform] | None = None,
373
- annotations: ToolAnnotations | None | NotSetT = NotSet,
374
- output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
375
- serializer: Callable[[Any], str] | None | NotSetT = NotSet,
376
- meta: dict[str, Any] | None | NotSetT = NotSet,
373
+ annotations: ToolAnnotations | NotSetT | None = NotSet,
374
+ output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
375
+ serializer: Callable[[Any], str] | NotSetT | None = NotSet,
376
+ meta: dict[str, Any] | NotSetT | None = NotSet,
377
377
  enabled: bool | None = None,
378
378
  ) -> TransformedTool:
379
379
  """Create a transformed tool from a parent tool.
fastmcp/utilities/cli.py CHANGED
@@ -150,22 +150,52 @@ _ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
150
150
  # █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
151
151
  # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
152
152
  LOGO_ASCII_2 = (
153
- "\033[38;2;0;198;255m \033[38;2;0;195;255m█\033[38;2;0;192;255m▀\033[38;2;0;189;255m▀\033[38;2;0;186;255m "
154
- "\033[38;2;0;184;255m▄\033[38;2;0;181;255m▀\033[38;2;0;178;255m█\033[38;2;0;175;255m "
155
- "\033[38;2;0;172;255m█\033[38;2;0;169;255m▀\033[38;2;0;166;255m▀\033[38;2;0;163;255m "
156
- "\033[38;2;0;160;255m▀\033[38;2;0;157;255m█\033[38;2;0;155;255m▀\033[38;2;0;152;255m "
157
- "\033[38;2;0;149;255m█\033[38;2;0;146;255m▀\033[38;2;0;143;255m▄\033[38;2;0;140;255m▀\033[38;2;0;137;255m█\033[38;2;0;134;255m "
158
- "\033[38;2;0;131;255m█\033[38;2;0;128;255m▀\033[38;2;0;126;255m▀\033[38;2;0;123;255m "
159
- "\033[38;2;0;120;255m█\033[38;2;0;117;255m▀\033[38;2;0;114;255m█\033[39m\n"
160
- "\033[38;2;0;198;255m \033[38;2;0;195;255m█\033[38;2;0;192;255m▀\033[38;2;0;189;255m \033[38;2;0;186;255m "
161
- "\033[38;2;0;184;255m█\033[38;2;0;181;255m▀\033[38;2;0;178;255m█\033[38;2;0;175;255m "
162
- "\033[38;2;0;172;255m▄\033[38;2;0;169;255m▄\033[38;2;0;166;255m█\033[38;2;0;163;255m "
163
- "\033[38;2;0;160;255m \033[38;2;0;157;255m█\033[38;2;0;155;255m \033[38;2;0;152;255m "
164
- "\033[38;2;0;149;255m█\033[38;2;0;146;255m \033[38;2;0;143;255m▀\033[38;2;0;140;255m \033[38;2;0;137;255m█\033[38;2;0;134;255m "
165
- "\033[38;2;0;131;255m█\033[38;2;0;128;255m▄\033[38;2;0;126;255m▄\033[38;2;0;123;255m "
166
- "\033[38;2;0;120;255m█\033[38;2;0;117;255m▀\033[38;2;0;114;255m▀\033[39m"
153
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m▀\x1b[38;2;0;186;255m "
154
+ "\x1b[38;2;0;184;255m▄\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
155
+ "\x1b[38;2;0;172;255m█\x1b[38;2;0;169;255m▀\x1b[38;2;0;166;255m▀\x1b[38;2;0;163;255m "
156
+ "\x1b[38;2;0;160;255m▀\x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m▀\x1b[38;2;0;152;255m "
157
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m▀\x1b[38;2;0;143;255m▄\x1b[38;2;0;140;255m▀\x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
158
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▀\x1b[38;2;0;126;255m▀\x1b[38;2;0;123;255m "
159
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m█\x1b[39m\n"
160
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m \x1b[38;2;0;186;255m "
161
+ "\x1b[38;2;0;184;255m█\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
162
+ "\x1b[38;2;0;172;255m▄\x1b[38;2;0;169;255m▄\x1b[38;2;0;166;255m█\x1b[38;2;0;163;255m "
163
+ "\x1b[38;2;0;160;255m \x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m \x1b[38;2;0;152;255m "
164
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m \x1b[38;2;0;143;255m▀\x1b[38;2;0;140;255m \x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
165
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▄\x1b[38;2;0;126;255m▄\x1b[38;2;0;123;255m "
166
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m▀\x1b[39m"
167
167
  ).strip()
168
168
 
169
+ # Prints the below in a blue gradient - sylized F
170
+ # ▄▀▀▀
171
+ # █▀▀
172
+ # ▀
173
+ LOGO_ASCII_3 = (
174
+ " \x1b[38;2;0;170;255m▄\x1b[38;2;0;142;255m▀\x1b[38;2;0;114;255m▀\x1b[38;2;0;86;255m▀\x1b[39m\n"
175
+ " \x1b[38;2;0;170;255m█\x1b[38;2;0;142;255m▀\x1b[38;2;0;114;255m▀\x1b[39m\n"
176
+ "\x1b[38;2;0;170;255m▀\x1b[39m\n"
177
+ "\x1b[0m"
178
+ )
179
+
180
+ # Prints the below in a blue gradient - block logo with slightly stylized F
181
+ # ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
182
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
183
+
184
+ LOGO_ASCII_4 = (
185
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m▄\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m▀\x1b[38;2;0;186;255m \x1b[38;2;0;184;255m▄\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
186
+ "\x1b[38;2;0;172;255m█\x1b[38;2;0;169;255m▀\x1b[38;2;0;166;255m▀\x1b[38;2;0;163;255m "
187
+ "\x1b[38;2;0;160;255m▀\x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m▀\x1b[38;2;0;152;255m "
188
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m▀\x1b[38;2;0;143;255m▄\x1b[38;2;0;140;255m▀\x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
189
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▀\x1b[38;2;0;126;255m▀\x1b[38;2;0;123;255m "
190
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m█\x1b[39m\n"
191
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m \x1b[38;2;0;186;255m \x1b[38;2;0;184;255m█\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
192
+ "\x1b[38;2;0;172;255m▄\x1b[38;2;0;169;255m▄\x1b[38;2;0;166;255m█\x1b[38;2;0;163;255m "
193
+ "\x1b[38;2;0;160;255m \x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m \x1b[38;2;0;152;255m "
194
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m \x1b[38;2;0;143;255m▀\x1b[38;2;0;140;255m \x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
195
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▄\x1b[38;2;0;126;255m▄\x1b[38;2;0;123;255m "
196
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m▀\x1b[39m\n"
197
+ )
198
+
169
199
 
170
200
  def log_server_banner(
171
201
  server: FastMCP[Any],
@@ -187,7 +217,7 @@ def log_server_banner(
187
217
 
188
218
  # Create the logo text
189
219
  # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
190
- logo_text = Text.from_ansi(LOGO_ASCII_2, no_wrap=True)
220
+ logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)
191
221
 
192
222
  # Create the main title
193
223
  title_text = Text(f"FastMCP {fastmcp.__version__}", style="bold blue")
@@ -210,12 +240,11 @@ def log_server_banner(
210
240
  info_table.add_row("📦", "Transport:", display_transport)
211
241
 
212
242
  # Show connection info based on transport
213
- if transport in ("http", "streamable-http", "sse"):
214
- if host and port:
215
- server_url = f"http://{host}:{port}"
216
- if path:
217
- server_url += f"/{path.lstrip('/')}"
218
- info_table.add_row("🔗", "Server URL:", server_url)
243
+ if transport in ("http", "streamable-http", "sse") and host and port:
244
+ server_url = f"http://{host}:{port}"
245
+ if path:
246
+ server_url += f"/{path.lstrip('/')}"
247
+ info_table.add_row("🔗", "Server URL:", server_url)
219
248
 
220
249
  # Add documentation link
221
250
  info_table.add_row("", "", "")
@@ -412,7 +412,7 @@ class InspectFormat(str, Enum):
412
412
  MCP = "mcp"
413
413
 
414
414
 
415
- async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
415
+ def format_fastmcp_info(info: FastMCPInfo) -> bytes:
416
416
  """Format FastMCPInfo as FastMCP-specific JSON.
417
417
 
418
418
  This includes FastMCP-specific fields like tags, enabled, annotations, etc.
@@ -501,6 +501,6 @@ async def format_info(
501
501
  # This works for both v1 and v2 servers
502
502
  if info is None:
503
503
  info = await inspect_fastmcp(mcp)
504
- return await format_fastmcp_info(info)
504
+ return format_fastmcp_info(info)
505
505
  else:
506
506
  raise ValueError(f"Unknown format: {format}")
@@ -61,7 +61,7 @@ from pydantic import (
61
61
  )
62
62
  from typing_extensions import NotRequired, TypedDict
63
63
 
64
- __all__ = ["json_schema_to_type", "JSONSchema"]
64
+ __all__ = ["JSONSchema", "json_schema_to_type"]
65
65
 
66
66
 
67
67
  FORMAT_TYPES: dict[str, Any] = {
@@ -368,7 +368,7 @@ def _schema_to_type(
368
368
  return types[0]
369
369
  else:
370
370
  if has_null:
371
- return Union[tuple(types + [type(None)])] # type: ignore # noqa: UP007
371
+ return Union[(*types, type(None))] # type: ignore
372
372
  else:
373
373
  return Union[tuple(types)] # type: ignore # noqa: UP007
374
374
 
@@ -389,7 +389,7 @@ def _schema_to_type(
389
389
  if len(types) == 1:
390
390
  return types[0] | None # type: ignore
391
391
  else:
392
- return Union[tuple(types + [type(None)])] # type: ignore # noqa: UP007
392
+ return Union[(*types, type(None))] # type: ignore
393
393
  return Union[tuple(types)] # type: ignore # noqa: UP007
394
394
 
395
395
  return _get_from_type_handler(schema, schemas)(schema)
@@ -578,7 +578,7 @@ def _create_dataclass(
578
578
  return _merge_defaults(data, original_schema)
579
579
  return data
580
580
 
581
- setattr(cls, "_apply_defaults", _apply_defaults)
581
+ cls._apply_defaults = _apply_defaults # type: ignore[attr-defined]
582
582
 
583
583
  # Store completed class
584
584
  _classes[cache_key] = cls
@@ -147,6 +147,18 @@ def temporary_log_level(
147
147
  yield
148
148
 
149
149
 
150
+ _level_to_no: dict[
151
+ Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None, int | None
152
+ ] = {
153
+ "DEBUG": logging.DEBUG,
154
+ "INFO": logging.INFO,
155
+ "WARNING": logging.WARNING,
156
+ "ERROR": logging.ERROR,
157
+ "CRITICAL": logging.CRITICAL,
158
+ None: None,
159
+ }
160
+
161
+
150
162
  class _ClampedLogFilter(logging.Filter):
151
163
  min_level: tuple[int, str] | None
152
164
  max_level: tuple[int, str] | None
@@ -161,29 +173,13 @@ class _ClampedLogFilter(logging.Filter):
161
173
  self.min_level = None
162
174
  self.max_level = None
163
175
 
164
- if min_level_no := self._level_to_no(level=min_level):
176
+ if min_level_no := _level_to_no.get(min_level):
165
177
  self.min_level = (min_level_no, str(min_level))
166
- if max_level_no := self._level_to_no(level=max_level):
178
+ if max_level_no := _level_to_no.get(max_level):
167
179
  self.max_level = (max_level_no, str(max_level))
168
180
 
169
181
  super().__init__()
170
182
 
171
- def _level_to_no(
172
- self, level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None
173
- ) -> int | None:
174
- if level == "DEBUG":
175
- return logging.DEBUG
176
- elif level == "INFO":
177
- return logging.INFO
178
- elif level == "WARNING":
179
- return logging.WARNING
180
- elif level == "ERROR":
181
- return logging.ERROR
182
- elif level == "CRITICAL":
183
- return logging.CRITICAL
184
- else:
185
- return None
186
-
187
183
  @override
188
184
  def filter(self, record: logging.LogRecord) -> bool:
189
185
  if self.max_level:
@@ -15,11 +15,11 @@ from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
15
15
  from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
16
16
 
17
17
  __all__ = [
18
- "Source",
19
18
  "Deployment",
20
19
  "Environment",
21
- "UVEnvironment",
22
- "MCPServerConfig",
23
20
  "FileSystemSource",
21
+ "MCPServerConfig",
22
+ "Source",
23
+ "UVEnvironment",
24
24
  "generate_schema",
25
25
  ]