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.
- fastmcp/cli/cli.py +7 -6
- fastmcp/cli/install/claude_code.py +6 -6
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/transports.py +77 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/resources/types.py +30 -24
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/handlers/authorize.py +324 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1256 -242
- fastmcp/server/auth/oidc_proxy.py +23 -6
- fastmcp/server/auth/providers/auth0.py +40 -21
- fastmcp/server/auth/providers/aws.py +29 -3
- fastmcp/server/auth/providers/azure.py +178 -127
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +29 -8
- fastmcp/server/auth/providers/google.py +30 -9
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +32 -14
- fastmcp/server/context.py +122 -36
- fastmcp/server/http.py +58 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +683 -207
- fastmcp/settings.py +24 -10
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +62 -22
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +617 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/METADATA +10 -6
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/RECORD +70 -63
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0.dist-info}/entry_points.txt +0 -0
- {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=
|
|
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
|
|
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))]
|
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
|
@@ -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
|
-
|
|
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,13 +230,13 @@ 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
|
|
@@ -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("🚀", "
|
|
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
|
-
|
|
274
|
+
# Center the panel itself
|
|
275
|
+
console.print(Group("\n", Align.center(panel), "\n"))
|
fastmcp/utilities/components.py
CHANGED
|
@@ -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.",
|