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.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -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
|
|
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:
|
|
30
|
+
transformations: Mapping[str, ToolTransformConfig] | None = None,
|
|
33
31
|
):
|
|
34
32
|
self._tools: dict[str, Tool] = {}
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
52
|
-
"""
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
raise e
|
|
230
|
-
|
|
231
|
-
|
|
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
|
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -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 |
|
|
369
|
-
description: str |
|
|
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 |
|
|
374
|
-
output_schema: dict[str, Any] |
|
|
375
|
-
serializer: Callable[[Any], str] |
|
|
376
|
-
meta: dict[str, Any] |
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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("
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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("🚀", "
|
|
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
|
-
|
|
273
|
+
# Center the panel itself
|
|
274
|
+
console.print(Group("\n", Align.center(panel), "\n"))
|
fastmcp/utilities/components.py
CHANGED
|
@@ -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):
|