fastmcp 2.12.5__py3-none-any.whl → 2.13.0__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 (72) hide show
  1. fastmcp/cli/cli.py +7 -6
  2. fastmcp/cli/install/claude_code.py +6 -6
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +85 -171
  12. fastmcp/client/transports.py +77 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/resources/types.py +30 -24
  24. fastmcp/server/auth/auth.py +40 -32
  25. fastmcp/server/auth/handlers/authorize.py +324 -0
  26. fastmcp/server/auth/jwt_issuer.py +236 -0
  27. fastmcp/server/auth/middleware.py +96 -0
  28. fastmcp/server/auth/oauth_proxy.py +1256 -242
  29. fastmcp/server/auth/oidc_proxy.py +23 -6
  30. fastmcp/server/auth/providers/auth0.py +40 -21
  31. fastmcp/server/auth/providers/aws.py +29 -3
  32. fastmcp/server/auth/providers/azure.py +178 -127
  33. fastmcp/server/auth/providers/descope.py +4 -6
  34. fastmcp/server/auth/providers/github.py +29 -8
  35. fastmcp/server/auth/providers/google.py +30 -9
  36. fastmcp/server/auth/providers/introspection.py +281 -0
  37. fastmcp/server/auth/providers/jwt.py +8 -2
  38. fastmcp/server/auth/providers/scalekit.py +179 -0
  39. fastmcp/server/auth/providers/supabase.py +172 -0
  40. fastmcp/server/auth/providers/workos.py +32 -14
  41. fastmcp/server/context.py +122 -36
  42. fastmcp/server/http.py +58 -18
  43. fastmcp/server/low_level.py +121 -2
  44. fastmcp/server/middleware/caching.py +469 -0
  45. fastmcp/server/middleware/error_handling.py +6 -2
  46. fastmcp/server/middleware/logging.py +48 -37
  47. fastmcp/server/middleware/middleware.py +28 -15
  48. fastmcp/server/middleware/rate_limiting.py +3 -3
  49. fastmcp/server/middleware/tool_injection.py +116 -0
  50. fastmcp/server/proxy.py +6 -6
  51. fastmcp/server/server.py +683 -207
  52. fastmcp/settings.py +24 -10
  53. fastmcp/tools/tool.py +7 -3
  54. fastmcp/tools/tool_manager.py +30 -112
  55. fastmcp/tools/tool_transform.py +3 -3
  56. fastmcp/utilities/cli.py +62 -22
  57. fastmcp/utilities/components.py +5 -0
  58. fastmcp/utilities/inspect.py +77 -21
  59. fastmcp/utilities/logging.py +118 -8
  60. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  61. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  62. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  63. fastmcp/utilities/tests.py +87 -4
  64. fastmcp/utilities/types.py +1 -1
  65. fastmcp/utilities/ui.py +617 -0
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
  68. fastmcp/cli/claude.py +0 -135
  69. fastmcp/utilities/storage.py +0 -204
  70. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
  71. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
  72. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py CHANGED
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import inspect
4
+ import os
4
5
  import warnings
5
6
  from pathlib import Path
6
7
  from typing import TYPE_CHECKING, Annotated, Any, Literal
7
8
 
9
+ from platformdirs import user_data_dir
8
10
  from pydantic import Field, ImportString, field_validator
9
11
  from pydantic.fields import FieldInfo
10
12
  from pydantic_settings import (
@@ -19,10 +21,14 @@ from fastmcp.utilities.logging import get_logger
19
21
 
20
22
  logger = get_logger(__name__)
21
23
 
24
+ ENV_FILE = os.getenv("FASTMCP_ENV_FILE", ".env")
25
+
22
26
  LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
23
27
 
24
28
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
25
29
 
30
+ TEN_MB_IN_BYTES = 1024 * 1024 * 10
31
+
26
32
  if TYPE_CHECKING:
27
33
  from fastmcp.server.auth.auth import AuthProvider
28
34
 
@@ -82,7 +88,7 @@ class Settings(BaseSettings):
82
88
 
83
89
  model_config = ExtendedSettingsConfigDict(
84
90
  env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
85
- env_file=".env",
91
+ env_file=ENV_FILE,
86
92
  extra="ignore",
87
93
  env_nested_delimiter="__",
88
94
  nested_model_default_partial_update=True,
@@ -145,7 +151,7 @@ class Settings(BaseSettings):
145
151
  )
146
152
  return self
147
153
 
148
- home: Path = Path.home() / ".fastmcp"
154
+ home: Path = Path(user_data_dir("fastmcp", appauthor=False))
149
155
 
150
156
  test_mode: bool = False
151
157
 
@@ -189,7 +195,6 @@ class Settings(BaseSettings):
189
195
  client_raise_first_exceptiongroup_error: Annotated[
190
196
  bool,
191
197
  Field(
192
- default=True,
193
198
  description=inspect.cleandoc(
194
199
  """
195
200
  Many MCP components operate in anyio taskgroups, and raise
@@ -205,7 +210,6 @@ class Settings(BaseSettings):
205
210
  resource_prefix_format: Annotated[
206
211
  Literal["protocol", "path"],
207
212
  Field(
208
- default="path",
209
213
  description=inspect.cleandoc(
210
214
  """
211
215
  When perfixing a resource URI, either use path formatting (resource://prefix/path)
@@ -235,7 +239,6 @@ class Settings(BaseSettings):
235
239
  mask_error_details: Annotated[
236
240
  bool,
237
241
  Field(
238
- default=False,
239
242
  description=inspect.cleandoc(
240
243
  """
241
244
  If True, error details from user-supplied functions (tool, resource, prompt)
@@ -248,6 +251,22 @@ class Settings(BaseSettings):
248
251
  ),
249
252
  ] = False
250
253
 
254
+ strict_input_validation: Annotated[
255
+ bool,
256
+ Field(
257
+ description=inspect.cleandoc(
258
+ """
259
+ If True, tool inputs are strictly validated against the input
260
+ JSON schema. For example, providing the string \"10\" to an
261
+ integer field will raise an error. If False, compatible inputs
262
+ will be coerced to match the schema, which can increase
263
+ compatibility. For example, providing the string \"10\" to an
264
+ integer field will be coerced to 10. Defaults to False.
265
+ """
266
+ ),
267
+ ),
268
+ ] = False
269
+
251
270
  server_dependencies: list[str] = Field(
252
271
  default_factory=list,
253
272
  description="List of dependencies to install in the server environment",
@@ -293,7 +312,6 @@ class Settings(BaseSettings):
293
312
  include_tags: Annotated[
294
313
  set[str] | None,
295
314
  Field(
296
- default=None,
297
315
  description=inspect.cleandoc(
298
316
  """
299
317
  If provided, only components that match these tags will be
@@ -306,7 +324,6 @@ class Settings(BaseSettings):
306
324
  exclude_tags: Annotated[
307
325
  set[str] | None,
308
326
  Field(
309
- default=None,
310
327
  description=inspect.cleandoc(
311
328
  """
312
329
  If provided, components that match these tags will be excluded
@@ -320,7 +337,6 @@ class Settings(BaseSettings):
320
337
  include_fastmcp_meta: Annotated[
321
338
  bool,
322
339
  Field(
323
- default=True,
324
340
  description=inspect.cleandoc(
325
341
  """
326
342
  Whether to include FastMCP meta in the server's MCP responses.
@@ -335,7 +351,6 @@ class Settings(BaseSettings):
335
351
  mounted_components_raise_on_load_error: Annotated[
336
352
  bool,
337
353
  Field(
338
- default=False,
339
354
  description=inspect.cleandoc(
340
355
  """
341
356
  If True, errors encountered when loading mounted components (tools, resources, prompts)
@@ -349,7 +364,6 @@ class Settings(BaseSettings):
349
364
  show_cli_banner: Annotated[
350
365
  bool,
351
366
  Field(
352
- default=True,
353
367
  description=inspect.cleandoc(
354
368
  """
355
369
  If True, the server banner will be displayed when running the server via CLI.
fastmcp/tools/tool.py CHANGED
@@ -16,7 +16,7 @@ from typing import (
16
16
 
17
17
  import mcp.types
18
18
  import pydantic_core
19
- from mcp.types import ContentBlock, TextContent, ToolAnnotations
19
+ from mcp.types import ContentBlock, Icon, TextContent, ToolAnnotations
20
20
  from mcp.types import Tool as MCPTool
21
21
  from pydantic import Field, PydanticSchemaGenerationError
22
22
  from typing_extensions import TypeVar
@@ -156,6 +156,7 @@ class Tool(FastMCPComponent):
156
156
  description=overrides.get("description", self.description),
157
157
  inputSchema=overrides.get("inputSchema", self.parameters),
158
158
  outputSchema=overrides.get("outputSchema", self.output_schema),
159
+ icons=overrides.get("icons", self.icons),
159
160
  annotations=overrides.get("annotations", self.annotations),
160
161
  _meta=overrides.get(
161
162
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -168,6 +169,7 @@ class Tool(FastMCPComponent):
168
169
  name: str | None = None,
169
170
  title: str | None = None,
170
171
  description: str | None = None,
172
+ icons: list[Icon] | None = None,
171
173
  tags: set[str] | None = None,
172
174
  annotations: ToolAnnotations | None = None,
173
175
  exclude_args: list[str] | None = None,
@@ -182,6 +184,7 @@ class Tool(FastMCPComponent):
182
184
  name=name,
183
185
  title=title,
184
186
  description=description,
187
+ icons=icons,
185
188
  tags=tags,
186
189
  annotations=annotations,
187
190
  exclude_args=exclude_args,
@@ -248,6 +251,7 @@ class FunctionTool(Tool):
248
251
  name: str | None = None,
249
252
  title: str | None = None,
250
253
  description: str | None = None,
254
+ icons: list[Icon] | None = None,
251
255
  tags: set[str] | None = None,
252
256
  annotations: ToolAnnotations | None = None,
253
257
  exclude_args: list[str] | None = None,
@@ -291,6 +295,7 @@ class FunctionTool(Tool):
291
295
  name=name or parsed_fn.name,
292
296
  title=title,
293
297
  description=description or parsed_fn.description,
298
+ icons=icons,
294
299
  parameters=parsed_fn.input_schema,
295
300
  output_schema=final_output_schema,
296
301
  annotations=annotations,
@@ -546,13 +551,12 @@ def _convert_to_content(
546
551
 
547
552
  # If any item is a ContentBlock, convert non-ContentBlock items to TextContent
548
553
  # without aggregating them
549
- if any(isinstance(item, ContentBlock) for item in result):
554
+ if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
550
555
  return [
551
556
  _convert_to_single_content_block(item, serializer)
552
557
  if not isinstance(item, ContentBlock)
553
558
  else item
554
559
  for item in result
555
560
  ]
556
-
557
561
  # If none of the items are ContentBlocks, aggregate all items into a single TextContent
558
562
  return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
- from collections.abc import Callable
5
- from typing import TYPE_CHECKING, Any
4
+ from collections.abc import Callable, Mapping
5
+ from typing import Any
6
6
 
7
7
  from mcp.types import ToolAnnotations
8
+ from pydantic import ValidationError
8
9
 
9
10
  from fastmcp import settings
10
11
  from fastmcp.exceptions import NotFoundError, ToolError
@@ -16,9 +17,6 @@ from fastmcp.tools.tool_transform import (
16
17
  )
17
18
  from fastmcp.utilities.logging import get_logger
18
19
 
19
- if TYPE_CHECKING:
20
- from fastmcp.server.server import MountedServer
21
-
22
20
  logger = get_logger(__name__)
23
21
 
24
22
 
@@ -29,12 +27,15 @@ class ToolManager:
29
27
  self,
30
28
  duplicate_behavior: DuplicateBehavior | None = None,
31
29
  mask_error_details: bool | None = None,
32
- transformations: dict[str, ToolTransformConfig] | None = None,
30
+ transformations: Mapping[str, ToolTransformConfig] | None = None,
33
31
  ):
34
32
  self._tools: dict[str, Tool] = {}
35
- self._mounted_servers: list[MountedServer] = []
36
- self.mask_error_details = mask_error_details or settings.mask_error_details
37
- 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
+ )
38
39
 
39
40
  # Default to "warn" if None is provided
40
41
  if duplicate_behavior is None:
@@ -48,56 +49,12 @@ class ToolManager:
48
49
 
49
50
  self.duplicate_behavior = duplicate_behavior
50
51
 
51
- def mount(self, server: MountedServer) -> None:
52
- """Adds a mounted server as a source for tools."""
53
- self._mounted_servers.append(server)
54
-
55
- async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
56
- """
57
- The single, consolidated recursive method for fetching tools. The 'via_server'
58
- parameter determines the communication path.
59
-
60
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
61
- - via_server=True: Server-to-server path for filtered MCP requests
62
- """
63
- all_tools: dict[str, Tool] = {}
64
-
65
- for mounted in self._mounted_servers:
66
- try:
67
- if via_server:
68
- # Use the server-to-server filtered path
69
- child_results = await mounted.server._list_tools()
70
- else:
71
- # Use the manager-to-manager unfiltered path
72
- child_results = await mounted.server._tool_manager.list_tools()
73
-
74
- # The combination logic is the same for both paths
75
- child_dict = {t.key: t for t in child_results}
76
- if mounted.prefix:
77
- for tool in child_dict.values():
78
- prefixed_tool = tool.model_copy(
79
- key=f"{mounted.prefix}_{tool.key}"
80
- )
81
- all_tools[prefixed_tool.key] = prefixed_tool
82
- else:
83
- all_tools.update(child_dict)
84
- except Exception as e:
85
- # Skip failed mounts silently, matches existing behavior
86
- logger.warning(
87
- f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
88
- )
89
- if settings.mounted_components_raise_on_load_error:
90
- raise
91
- continue
92
-
93
- # Finally, add local tools, which always take precedence
94
- all_tools.update(self._tools)
95
-
52
+ async def _load_tools(self) -> dict[str, Tool]:
53
+ """Return this manager's local tools with transformations applied."""
96
54
  transformed_tools = apply_transformations_to_tools(
97
- tools=all_tools,
55
+ tools=self._tools,
98
56
  transformations=self.transformations,
99
57
  )
100
-
101
58
  return transformed_tools
102
59
 
103
60
  async def has_tool(self, key: str) -> bool:
@@ -114,25 +71,9 @@ class ToolManager:
114
71
 
115
72
  async def get_tools(self) -> dict[str, Tool]:
116
73
  """
117
- Gets the complete, unfiltered inventory of all tools.
118
- """
119
- return await self._load_tools(via_server=False)
120
-
121
- async def list_tools(self) -> list[Tool]:
74
+ Gets the complete, unfiltered inventory of local tools.
122
75
  """
123
- Lists all tools, applying protocol filtering.
124
- """
125
- tools_dict = await self._load_tools(via_server=True)
126
- return list(tools_dict.values())
127
-
128
- @property
129
- def _tools_transformed(self) -> list[str]:
130
- """Get the local tools."""
131
-
132
- return [
133
- transformation.name or tool_name
134
- for tool_name, transformation in self.transformations.items()
135
- ]
76
+ return await self._load_tools()
136
77
 
137
78
  def add_tool_from_fn(
138
79
  self,
@@ -214,41 +155,18 @@ class ToolManager:
214
155
  Internal API for servers: Finds and calls a tool, respecting the
215
156
  filtered protocol path.
216
157
  """
217
- # 1. Check local tools first. The server will have already applied its filter.
218
- if key in self._tools or key in self._tools_transformed:
219
- tool = await self.get_tool(key)
220
- if not tool:
221
- raise NotFoundError(f"Tool {key!r} not found")
222
-
223
- try:
224
- return await tool.run(arguments)
225
-
226
- # raise ToolErrors as-is
227
- except ToolError as e:
228
- logger.exception(f"Error calling tool {key!r}")
229
- raise e
230
-
231
- # Handle other exceptions
232
- except Exception as e:
233
- logger.exception(f"Error calling tool {key!r}")
234
- if self.mask_error_details:
235
- # Mask internal details
236
- raise ToolError(f"Error calling tool {key!r}") from e
237
- else:
238
- # Include original error details
239
- raise ToolError(f"Error calling tool {key!r}: {e}") from e
240
-
241
- # 2. Check mounted servers using the filtered protocol path.
242
- for mounted in reversed(self._mounted_servers):
243
- tool_key = key
244
- if mounted.prefix:
245
- if key.startswith(f"{mounted.prefix}_"):
246
- tool_key = key.removeprefix(f"{mounted.prefix}_")
247
- else:
248
- continue
249
- try:
250
- return await mounted.server._call_tool(tool_key, arguments)
251
- except NotFoundError:
252
- continue
253
-
254
- raise NotFoundError(f"Tool {key!r} not found.")
158
+ tool = await self.get_tool(key)
159
+ try:
160
+ return await tool.run(arguments)
161
+ except ValidationError as e:
162
+ logger.exception(f"Error validating tool {key!r}: {e}")
163
+ raise e
164
+ except ToolError as e:
165
+ logger.exception(f"Error calling tool {key!r}")
166
+ raise e
167
+ except Exception as e:
168
+ logger.exception(f"Error calling tool {key!r}")
169
+ if self.mask_error_details:
170
+ raise ToolError(f"Error calling tool {key!r}") from e
171
+ else:
172
+ raise ToolError(f"Error calling tool {key!r}: {e}") from e
@@ -34,7 +34,7 @@ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[
34
34
  )
35
35
 
36
36
 
37
- async def forward(**kwargs) -> ToolResult:
37
+ async def forward(**kwargs: Any) -> ToolResult:
38
38
  """Forward to parent tool with argument transformation applied.
39
39
 
40
40
  This function can only be called from within a transformed tool's custom
@@ -64,7 +64,7 @@ async def forward(**kwargs) -> ToolResult:
64
64
  return await tool.forwarding_fn(**kwargs)
65
65
 
66
66
 
67
- async def forward_raw(**kwargs) -> ToolResult:
67
+ async def forward_raw(**kwargs: Any) -> ToolResult:
68
68
  """Forward directly to parent tool without transformation.
69
69
 
70
70
  This function bypasses all argument transformation and validation, calling the parent
@@ -681,7 +681,7 @@ class TransformedTool(Tool):
681
681
  schema = compress_schema(schema, prune_defs=True)
682
682
 
683
683
  # Create forwarding function that closes over everything it needs
684
- async def _forward(**kwargs):
684
+ async def _forward(**kwargs: Any):
685
685
  # Validate arguments
686
686
  valid_args = set(new_props.keys())
687
687
  provided_args = set(kwargs.keys())
fastmcp/utilities/cli.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
- from importlib.metadata import version
6
5
  from pathlib import Path
7
6
  from typing import TYPE_CHECKING, Any, Literal
8
7
 
@@ -138,7 +137,7 @@ def load_and_merge_config(
138
137
  return new_config, resolved_spec
139
138
 
140
139
 
141
- LOGO_ASCII = r"""
140
+ LOGO_ASCII_1 = r"""
142
141
  _ __ ___ _____ __ __ _____________ ____ ____
143
142
  _ __ ___ .'____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
144
143
  _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
@@ -147,6 +146,56 @@ _ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
147
146
 
148
147
  """.lstrip("\n")
149
148
 
149
+ # This prints the below in a blue gradient
150
+ # █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
151
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
152
+ LOGO_ASCII_2 = (
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
+ ).strip()
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
+
150
199
 
151
200
  def log_server_banner(
152
201
  server: FastMCP[Any],
@@ -167,10 +216,11 @@ def log_server_banner(
167
216
  """
168
217
 
169
218
  # Create the logo text
170
- logo_text = Text(LOGO_ASCII, style="bold green")
219
+ # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
220
+ logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)
171
221
 
172
222
  # Create the main title
173
- title_text = Text("FastMCP 2.0", style="bold blue")
223
+ title_text = Text(f"FastMCP {fastmcp.__version__}", style="bold blue")
174
224
 
175
225
  # Create the information table
176
226
  info_table = Table.grid(padding=(0, 1))
@@ -180,13 +230,13 @@ def log_server_banner(
180
230
 
181
231
  match transport:
182
232
  case "http" | "streamable-http":
183
- display_transport = "Streamable-HTTP"
233
+ display_transport = "HTTP"
184
234
  case "sse":
185
235
  display_transport = "SSE"
186
236
  case "stdio":
187
237
  display_transport = "STDIO"
188
238
 
189
- info_table.add_row("🖥️", "Server name:", server.name)
239
+ info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
190
240
  info_table.add_row("📦", "Transport:", display_transport)
191
241
 
192
242
  # Show connection info based on transport
@@ -197,27 +247,15 @@ def log_server_banner(
197
247
  server_url += f"/{path.lstrip('/')}"
198
248
  info_table.add_row("🔗", "Server URL:", server_url)
199
249
 
200
- # Add version information with explicit style overrides
201
- info_table.add_row("", "", "")
202
- info_table.add_row(
203
- "🏎️",
204
- "FastMCP version:",
205
- Text(fastmcp.__version__, style="dim white", no_wrap=True),
206
- )
207
- info_table.add_row(
208
- "🤝",
209
- "MCP SDK version:",
210
- Text(version("mcp"), style="dim white", no_wrap=True),
211
- )
212
-
213
250
  # Add documentation link
214
251
  info_table.add_row("", "", "")
215
252
  info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
216
- info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
253
+ info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
217
254
 
218
255
  # Create panel with logo, title, and information using Group
219
256
  panel_content = Group(
220
257
  Align.center(logo_text),
258
+ "",
221
259
  Align.center(title_text),
222
260
  "",
223
261
  "",
@@ -228,8 +266,10 @@ def log_server_banner(
228
266
  panel_content,
229
267
  border_style="dim",
230
268
  padding=(1, 4),
231
- expand=False,
269
+ # expand=False,
270
+ width=80, # Set max width for the panel
232
271
  )
233
272
 
234
273
  console = Console(stderr=True)
235
- console.print(Group("\n", panel, "\n"))
274
+ # Center the panel itself
275
+ console.print(Group("\n", Align.center(panel), "\n"))
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Sequence
4
4
  from typing import Annotated, Any, TypedDict
5
5
 
6
+ from mcp.types import Icon
6
7
  from pydantic import BeforeValidator, Field, PrivateAttr
7
8
  from typing_extensions import Self, TypeVar
8
9
 
@@ -39,6 +40,10 @@ class FastMCPComponent(FastMCPBaseModel):
39
40
  default=None,
40
41
  description="The description of the component.",
41
42
  )
43
+ icons: list[Icon] | None = Field(
44
+ default=None,
45
+ description="Optional list of icons for this component to display in user interfaces.",
46
+ )
42
47
  tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(
43
48
  default_factory=set,
44
49
  description="Tags for the component.",