fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc2__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 (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  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 +81 -171
  12. fastmcp/client/transports.py +76 -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/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1228 -233
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +13 -7
  28. fastmcp/server/auth/providers/aws.py +14 -3
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +14 -8
  32. fastmcp/server/auth/providers/google.py +15 -9
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +17 -14
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +57 -17
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +32 -22
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/licenses/LICENSE +0 -0
fastmcp/settings.py CHANGED
@@ -1,6 +1,7 @@
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
@@ -19,10 +20,14 @@ from fastmcp.utilities.logging import get_logger
19
20
 
20
21
  logger = get_logger(__name__)
21
22
 
23
+ ENV_FILE = os.getenv("FASTMCP_ENV_FILE", ".env")
24
+
22
25
  LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
23
26
 
24
27
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
25
28
 
29
+ TEN_MB_IN_BYTES = 1024 * 1024 * 10
30
+
26
31
  if TYPE_CHECKING:
27
32
  from fastmcp.server.auth.auth import AuthProvider
28
33
 
@@ -82,7 +87,7 @@ class Settings(BaseSettings):
82
87
 
83
88
  model_config = ExtendedSettingsConfigDict(
84
89
  env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
85
- env_file=".env",
90
+ env_file=ENV_FILE,
86
91
  extra="ignore",
87
92
  env_nested_delimiter="__",
88
93
  nested_model_default_partial_update=True,
@@ -189,7 +194,6 @@ class Settings(BaseSettings):
189
194
  client_raise_first_exceptiongroup_error: Annotated[
190
195
  bool,
191
196
  Field(
192
- default=True,
193
197
  description=inspect.cleandoc(
194
198
  """
195
199
  Many MCP components operate in anyio taskgroups, and raise
@@ -205,7 +209,6 @@ class Settings(BaseSettings):
205
209
  resource_prefix_format: Annotated[
206
210
  Literal["protocol", "path"],
207
211
  Field(
208
- default="path",
209
212
  description=inspect.cleandoc(
210
213
  """
211
214
  When perfixing a resource URI, either use path formatting (resource://prefix/path)
@@ -235,7 +238,6 @@ class Settings(BaseSettings):
235
238
  mask_error_details: Annotated[
236
239
  bool,
237
240
  Field(
238
- default=False,
239
241
  description=inspect.cleandoc(
240
242
  """
241
243
  If True, error details from user-supplied functions (tool, resource, prompt)
@@ -248,6 +250,22 @@ class Settings(BaseSettings):
248
250
  ),
249
251
  ] = False
250
252
 
253
+ strict_input_validation: Annotated[
254
+ bool,
255
+ Field(
256
+ description=inspect.cleandoc(
257
+ """
258
+ If True, tool inputs are strictly validated against the input
259
+ JSON schema. For example, providing the string \"10\" to an
260
+ integer field will raise an error. If False, compatible inputs
261
+ will be coerced to match the schema, which can increase
262
+ compatibility. For example, providing the string \"10\" to an
263
+ integer field will be coerced to 10. Defaults to False.
264
+ """
265
+ ),
266
+ ),
267
+ ] = False
268
+
251
269
  server_dependencies: list[str] = Field(
252
270
  default_factory=list,
253
271
  description="List of dependencies to install in the server environment",
@@ -293,7 +311,6 @@ class Settings(BaseSettings):
293
311
  include_tags: Annotated[
294
312
  set[str] | None,
295
313
  Field(
296
- default=None,
297
314
  description=inspect.cleandoc(
298
315
  """
299
316
  If provided, only components that match these tags will be
@@ -306,7 +323,6 @@ class Settings(BaseSettings):
306
323
  exclude_tags: Annotated[
307
324
  set[str] | None,
308
325
  Field(
309
- default=None,
310
326
  description=inspect.cleandoc(
311
327
  """
312
328
  If provided, components that match these tags will be excluded
@@ -320,7 +336,6 @@ class Settings(BaseSettings):
320
336
  include_fastmcp_meta: Annotated[
321
337
  bool,
322
338
  Field(
323
- default=True,
324
339
  description=inspect.cleandoc(
325
340
  """
326
341
  Whether to include FastMCP meta in the server's MCP responses.
@@ -335,7 +350,6 @@ class Settings(BaseSettings):
335
350
  mounted_components_raise_on_load_error: Annotated[
336
351
  bool,
337
352
  Field(
338
- default=False,
339
353
  description=inspect.cleandoc(
340
354
  """
341
355
  If True, errors encountered when loading mounted components (tools, resources, prompts)
@@ -349,7 +363,6 @@ class Settings(BaseSettings):
349
363
  show_cli_banner: Annotated[
350
364
  bool,
351
365
  Field(
352
- default=True,
353
366
  description=inspect.cleandoc(
354
367
  """
355
368
  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))]
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import warnings
4
4
  from collections.abc import Callable
5
- from typing import TYPE_CHECKING, Any
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
 
@@ -32,7 +30,6 @@ class ToolManager:
32
30
  transformations: dict[str, ToolTransformConfig] | None = None,
33
31
  ):
34
32
  self._tools: dict[str, Tool] = {}
35
- self._mounted_servers: list[MountedServer] = []
36
33
  self.mask_error_details = mask_error_details or settings.mask_error_details
37
34
  self.transformations = transformations or {}
38
35
 
@@ -48,56 +45,12 @@ class ToolManager:
48
45
 
49
46
  self.duplicate_behavior = duplicate_behavior
50
47
 
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
-
48
+ async def _load_tools(self) -> dict[str, Tool]:
49
+ """Return this manager's local tools with transformations applied."""
96
50
  transformed_tools = apply_transformations_to_tools(
97
- tools=all_tools,
51
+ tools=self._tools,
98
52
  transformations=self.transformations,
99
53
  )
100
-
101
54
  return transformed_tools
102
55
 
103
56
  async def has_tool(self, key: str) -> bool:
@@ -114,25 +67,9 @@ class ToolManager:
114
67
 
115
68
  async def get_tools(self) -> dict[str, Tool]:
116
69
  """
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]:
122
- """
123
- Lists all tools, applying protocol filtering.
70
+ Gets the complete, unfiltered inventory of local tools.
124
71
  """
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
- ]
72
+ return await self._load_tools()
136
73
 
137
74
  def add_tool_from_fn(
138
75
  self,
@@ -214,41 +151,18 @@ class ToolManager:
214
151
  Internal API for servers: Finds and calls a tool, respecting the
215
152
  filtered protocol path.
216
153
  """
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.")
154
+ tool = await self.get_tool(key)
155
+ try:
156
+ return await tool.run(arguments)
157
+ except ValidationError as e:
158
+ logger.exception(f"Error validating tool {key!r}: {e}")
159
+ raise e
160
+ except ToolError as e:
161
+ logger.exception(f"Error calling tool {key!r}")
162
+ raise e
163
+ except Exception as e:
164
+ logger.exception(f"Error calling tool {key!r}")
165
+ if self.mask_error_details:
166
+ raise ToolError(f"Error calling tool {key!r}") from e
167
+ else:
168
+ 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,26 @@ _ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
147
146
 
148
147
  """.lstrip("\n")
149
148
 
149
+ # This prints the below in a blue gradient
150
+ # █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
151
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
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"
167
+ ).strip()
168
+
150
169
 
151
170
  def log_server_banner(
152
171
  server: FastMCP[Any],
@@ -167,10 +186,11 @@ def log_server_banner(
167
186
  """
168
187
 
169
188
  # Create the logo text
170
- logo_text = Text(LOGO_ASCII, style="bold green")
189
+ # 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)
171
191
 
172
192
  # Create the main title
173
- title_text = Text("FastMCP 2.0", style="bold blue")
193
+ title_text = Text(f"FastMCP {fastmcp.__version__}", style="bold blue")
174
194
 
175
195
  # Create the information table
176
196
  info_table = Table.grid(padding=(0, 1))
@@ -180,13 +200,13 @@ def log_server_banner(
180
200
 
181
201
  match transport:
182
202
  case "http" | "streamable-http":
183
- display_transport = "Streamable-HTTP"
203
+ display_transport = "HTTP"
184
204
  case "sse":
185
205
  display_transport = "SSE"
186
206
  case "stdio":
187
207
  display_transport = "STDIO"
188
208
 
189
- info_table.add_row("🖥️", "Server name:", server.name)
209
+ info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
190
210
  info_table.add_row("📦", "Transport:", display_transport)
191
211
 
192
212
  # Show connection info based on transport
@@ -197,27 +217,15 @@ def log_server_banner(
197
217
  server_url += f"/{path.lstrip('/')}"
198
218
  info_table.add_row("🔗", "Server URL:", server_url)
199
219
 
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
220
  # Add documentation link
214
221
  info_table.add_row("", "", "")
215
222
  info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
216
- info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
223
+ info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
217
224
 
218
225
  # Create panel with logo, title, and information using Group
219
226
  panel_content = Group(
220
227
  Align.center(logo_text),
228
+ "",
221
229
  Align.center(title_text),
222
230
  "",
223
231
  "",
@@ -228,8 +236,10 @@ def log_server_banner(
228
236
  panel_content,
229
237
  border_style="dim",
230
238
  padding=(1, 4),
231
- expand=False,
239
+ # expand=False,
240
+ width=80, # Set max width for the panel
232
241
  )
233
242
 
234
243
  console = Console(stderr=True)
235
- console.print(Group("\n", panel, "\n"))
244
+ # Center the panel itself
245
+ 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.",