fastmcp 2.12.1__py3-none-any.whl → 2.13.2__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 (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -4,6 +4,7 @@ import inspect
4
4
  import warnings
5
5
  from collections.abc import Callable
6
6
  from contextvars import ContextVar
7
+ from copy import deepcopy
7
8
  from dataclasses import dataclass
8
9
  from typing import Annotated, Any, Literal, cast
9
10
 
@@ -34,7 +35,7 @@ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[
34
35
  )
35
36
 
36
37
 
37
- async def forward(**kwargs) -> ToolResult:
38
+ async def forward(**kwargs: Any) -> ToolResult:
38
39
  """Forward to parent tool with argument transformation applied.
39
40
 
40
41
  This function can only be called from within a transformed tool's custom
@@ -64,7 +65,7 @@ async def forward(**kwargs) -> ToolResult:
64
65
  return await tool.forwarding_fn(**kwargs)
65
66
 
66
67
 
67
- async def forward_raw(**kwargs) -> ToolResult:
68
+ async def forward_raw(**kwargs: Any) -> ToolResult:
68
69
  """Forward directly to parent tool without transformation.
69
70
 
70
71
  This function bypasses all argument transformation and validation, calling the parent
@@ -365,15 +366,15 @@ class TransformedTool(Tool):
365
366
  cls,
366
367
  tool: Tool,
367
368
  name: str | None = None,
368
- title: str | None | NotSetT = NotSet,
369
- description: str | None | NotSetT = NotSet,
369
+ title: str | NotSetT | None = NotSet,
370
+ description: str | NotSetT | None = NotSet,
370
371
  tags: set[str] | None = None,
371
372
  transform_fn: Callable[..., Any] | None = None,
372
373
  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,
374
+ annotations: ToolAnnotations | NotSetT | None = NotSet,
375
+ output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet,
376
+ serializer: Callable[[Any], str] | NotSetT | None = NotSet,
377
+ meta: dict[str, Any] | NotSetT | None = NotSet,
377
378
  enabled: bool | None = None,
378
379
  ) -> TransformedTool:
379
380
  """Create a transformed tool from a parent tool.
@@ -620,7 +621,8 @@ class TransformedTool(Tool):
620
621
  """
621
622
 
622
623
  # Build transformed schema and mapping
623
- parent_defs = parent_tool.parameters.get("$defs", {})
624
+ # Deep copy to prevent compress_schema from mutating parent tool's $defs
625
+ parent_defs = deepcopy(parent_tool.parameters.get("$defs", {}))
624
626
  parent_props = parent_tool.parameters.get("properties", {}).copy()
625
627
  parent_required = set(parent_tool.parameters.get("required", []))
626
628
 
@@ -681,7 +683,7 @@ class TransformedTool(Tool):
681
683
  schema = compress_schema(schema, prune_defs=True)
682
684
 
683
685
  # Create forwarding function that closes over everything it needs
684
- async def _forward(**kwargs):
686
+ async def _forward(**kwargs: Any):
685
687
  # Validate arguments
686
688
  valid_args = set(new_props.keys())
687
689
  provided_args = set(kwargs.keys())
@@ -934,7 +936,7 @@ def apply_transformations_to_tools(
934
936
  tools: dict[str, Tool],
935
937
  transformations: dict[str, ToolTransformConfig],
936
938
  ) -> dict[str, Tool]:
937
- """Apply a list of transformations to a list of tools. Tools that do not have any transforamtions
939
+ """Apply a list of transformations to a list of tools. Tools that do not have any transformations
938
940
  are left unchanged.
939
941
  """
940
942
 
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,44 +230,31 @@ 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
193
- if transport in ("http", "streamable-http", "sse"):
194
- if host and port:
195
- server_url = f"http://{host}:{port}"
196
- if path:
197
- server_url += f"/{path.lstrip('/')}"
198
- info_table.add_row("🔗", "Server URL:", server_url)
199
-
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
- )
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)
212
248
 
213
249
  # Add documentation link
214
250
  info_table.add_row("", "", "")
215
251
  info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
216
- info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
252
+ info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
217
253
 
218
254
  # Create panel with logo, title, and information using Group
219
255
  panel_content = Group(
220
256
  Align.center(logo_text),
257
+ "",
221
258
  Align.center(title_text),
222
259
  "",
223
260
  "",
@@ -228,8 +265,10 @@ def log_server_banner(
228
265
  panel_content,
229
266
  border_style="dim",
230
267
  padding=(1, 4),
231
- expand=False,
268
+ # expand=False,
269
+ width=80, # Set max width for the panel
232
270
  )
233
271
 
234
272
  console = Console(stderr=True)
235
- console.print(Group("\n", panel, "\n"))
273
+ # Center the panel itself
274
+ console.print(Group("\n", Align.center(panel), "\n"))
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import Annotated, Any, TypedDict
4
+ from typing import Annotated, Any, TypedDict, cast
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.",
@@ -112,7 +117,7 @@ class FastMCPComponent(FastMCPBaseModel):
112
117
  copy = super().model_copy(update=update, deep=deep)
113
118
  if key is not None:
114
119
  copy._key = key
115
- return copy
120
+ return cast(Self, copy)
116
121
 
117
122
  def __eq__(self, other: object) -> bool:
118
123
  if type(self) is not type(other):