fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc1__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 +6 -6
- fastmcp/cli/install/claude_code.py +3 -3
- 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 +81 -171
- fastmcp/client/transports.py +76 -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/server/auth/auth.py +40 -32
- fastmcp/server/auth/jwt_issuer.py +289 -0
- fastmcp/server/auth/oauth_proxy.py +1238 -234
- fastmcp/server/auth/oidc_proxy.py +8 -6
- fastmcp/server/auth/providers/auth0.py +12 -6
- fastmcp/server/auth/providers/aws.py +13 -2
- fastmcp/server/auth/providers/azure.py +137 -124
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +13 -7
- fastmcp/server/auth/providers/google.py +13 -7
- 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 +16 -13
- fastmcp/server/context.py +89 -34
- fastmcp/server/http.py +53 -16
- 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/proxy.py +6 -6
- fastmcp/server/server.py +638 -183
- fastmcp/settings.py +22 -9
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +22 -108
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +2 -2
- 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 +497 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc1.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=
|
|
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))]
|
fastmcp/tools/tool_manager.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.")
|
|
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
|
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
|
@@ -186,7 +186,7 @@ def log_server_banner(
|
|
|
186
186
|
case "stdio":
|
|
187
187
|
display_transport = "STDIO"
|
|
188
188
|
|
|
189
|
-
info_table.add_row("
|
|
189
|
+
info_table.add_row("🖥", "Server name:", server.name)
|
|
190
190
|
info_table.add_row("📦", "Transport:", display_transport)
|
|
191
191
|
|
|
192
192
|
# Show connection info based on transport
|
|
@@ -200,7 +200,7 @@ def log_server_banner(
|
|
|
200
200
|
# Add version information with explicit style overrides
|
|
201
201
|
info_table.add_row("", "", "")
|
|
202
202
|
info_table.add_row(
|
|
203
|
-
"
|
|
203
|
+
"🏎",
|
|
204
204
|
"FastMCP version:",
|
|
205
205
|
Text(fastmcp.__version__, style="dim white", no_wrap=True),
|
|
206
206
|
)
|
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.",
|
fastmcp/utilities/inspect.py
CHANGED
|
@@ -28,6 +28,7 @@ class ToolInfo:
|
|
|
28
28
|
tags: list[str] | None = None
|
|
29
29
|
enabled: bool | None = None
|
|
30
30
|
title: str | None = None
|
|
31
|
+
icons: list[dict[str, Any]] | None = None
|
|
31
32
|
meta: dict[str, Any] | None = None
|
|
32
33
|
|
|
33
34
|
|
|
@@ -42,6 +43,7 @@ class PromptInfo:
|
|
|
42
43
|
tags: list[str] | None = None
|
|
43
44
|
enabled: bool | None = None
|
|
44
45
|
title: str | None = None
|
|
46
|
+
icons: list[dict[str, Any]] | None = None
|
|
45
47
|
meta: dict[str, Any] | None = None
|
|
46
48
|
|
|
47
49
|
|
|
@@ -58,6 +60,7 @@ class ResourceInfo:
|
|
|
58
60
|
tags: list[str] | None = None
|
|
59
61
|
enabled: bool | None = None
|
|
60
62
|
title: str | None = None
|
|
63
|
+
icons: list[dict[str, Any]] | None = None
|
|
61
64
|
meta: dict[str, Any] | None = None
|
|
62
65
|
|
|
63
66
|
|
|
@@ -75,6 +78,7 @@ class TemplateInfo:
|
|
|
75
78
|
tags: list[str] | None = None
|
|
76
79
|
enabled: bool | None = None
|
|
77
80
|
title: str | None = None
|
|
81
|
+
icons: list[dict[str, Any]] | None = None
|
|
78
82
|
meta: dict[str, Any] | None = None
|
|
79
83
|
|
|
80
84
|
|
|
@@ -85,6 +89,8 @@ class FastMCPInfo:
|
|
|
85
89
|
name: str
|
|
86
90
|
instructions: str | None
|
|
87
91
|
version: str | None # The server's own version string (if specified)
|
|
92
|
+
website_url: str | None
|
|
93
|
+
icons: list[dict[str, Any]] | None
|
|
88
94
|
fastmcp_version: str # Version of FastMCP generating this manifest
|
|
89
95
|
mcp_version: str # Version of MCP protocol library
|
|
90
96
|
server_generation: int # Server generation: 1 (mcp package) or 2 (fastmcp)
|
|
@@ -104,21 +110,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
104
110
|
Returns:
|
|
105
111
|
FastMCPInfo dataclass containing the extracted information
|
|
106
112
|
"""
|
|
107
|
-
# Get all
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
# Get all components via middleware to respect filtering and preserve metadata
|
|
114
|
+
tools_list = await mcp._list_tools_middleware()
|
|
115
|
+
prompts_list = await mcp._list_prompts_middleware()
|
|
116
|
+
resources_list = await mcp._list_resources_middleware()
|
|
117
|
+
templates_list = await mcp._list_resource_templates_middleware()
|
|
112
118
|
|
|
113
119
|
# Extract detailed tool information
|
|
114
120
|
tool_infos = []
|
|
115
|
-
for
|
|
116
|
-
|
|
117
|
-
mcp_tool = tool.to_mcp_tool(name=key)
|
|
121
|
+
for tool in tools_list:
|
|
122
|
+
mcp_tool = tool.to_mcp_tool(name=tool.key)
|
|
118
123
|
tool_infos.append(
|
|
119
124
|
ToolInfo(
|
|
120
|
-
key=key,
|
|
121
|
-
name=tool.name or key,
|
|
125
|
+
key=tool.key,
|
|
126
|
+
name=tool.name or tool.key,
|
|
122
127
|
description=tool.description,
|
|
123
128
|
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
|
|
124
129
|
output_schema=tool.output_schema,
|
|
@@ -126,17 +131,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
126
131
|
tags=list(tool.tags) if tool.tags else None,
|
|
127
132
|
enabled=tool.enabled,
|
|
128
133
|
title=tool.title,
|
|
134
|
+
icons=[icon.model_dump() for icon in tool.icons]
|
|
135
|
+
if tool.icons
|
|
136
|
+
else None,
|
|
129
137
|
meta=tool.meta,
|
|
130
138
|
)
|
|
131
139
|
)
|
|
132
140
|
|
|
133
141
|
# Extract detailed prompt information
|
|
134
142
|
prompt_infos = []
|
|
135
|
-
for
|
|
143
|
+
for prompt in prompts_list:
|
|
136
144
|
prompt_infos.append(
|
|
137
145
|
PromptInfo(
|
|
138
|
-
key=key,
|
|
139
|
-
name=prompt.name or key,
|
|
146
|
+
key=prompt.key,
|
|
147
|
+
name=prompt.name or prompt.key,
|
|
140
148
|
description=prompt.description,
|
|
141
149
|
arguments=[arg.model_dump() for arg in prompt.arguments]
|
|
142
150
|
if prompt.arguments
|
|
@@ -144,17 +152,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
144
152
|
tags=list(prompt.tags) if prompt.tags else None,
|
|
145
153
|
enabled=prompt.enabled,
|
|
146
154
|
title=prompt.title,
|
|
155
|
+
icons=[icon.model_dump() for icon in prompt.icons]
|
|
156
|
+
if prompt.icons
|
|
157
|
+
else None,
|
|
147
158
|
meta=prompt.meta,
|
|
148
159
|
)
|
|
149
160
|
)
|
|
150
161
|
|
|
151
162
|
# Extract detailed resource information
|
|
152
163
|
resource_infos = []
|
|
153
|
-
for
|
|
164
|
+
for resource in resources_list:
|
|
154
165
|
resource_infos.append(
|
|
155
166
|
ResourceInfo(
|
|
156
|
-
key=key,
|
|
157
|
-
uri=key,
|
|
167
|
+
key=resource.key,
|
|
168
|
+
uri=resource.key,
|
|
158
169
|
name=resource.name,
|
|
159
170
|
description=resource.description,
|
|
160
171
|
mime_type=resource.mime_type,
|
|
@@ -164,17 +175,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
164
175
|
tags=list(resource.tags) if resource.tags else None,
|
|
165
176
|
enabled=resource.enabled,
|
|
166
177
|
title=resource.title,
|
|
178
|
+
icons=[icon.model_dump() for icon in resource.icons]
|
|
179
|
+
if resource.icons
|
|
180
|
+
else None,
|
|
167
181
|
meta=resource.meta,
|
|
168
182
|
)
|
|
169
183
|
)
|
|
170
184
|
|
|
171
185
|
# Extract detailed template information
|
|
172
186
|
template_infos = []
|
|
173
|
-
for
|
|
187
|
+
for template in templates_list:
|
|
174
188
|
template_infos.append(
|
|
175
189
|
TemplateInfo(
|
|
176
|
-
key=key,
|
|
177
|
-
uri_template=key,
|
|
190
|
+
key=template.key,
|
|
191
|
+
uri_template=template.key,
|
|
178
192
|
name=template.name,
|
|
179
193
|
description=template.description,
|
|
180
194
|
mime_type=template.mime_type,
|
|
@@ -185,6 +199,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
185
199
|
tags=list(template.tags) if template.tags else None,
|
|
186
200
|
enabled=template.enabled,
|
|
187
201
|
title=template.title,
|
|
202
|
+
icons=[icon.model_dump() for icon in template.icons]
|
|
203
|
+
if template.icons
|
|
204
|
+
else None,
|
|
188
205
|
meta=template.meta,
|
|
189
206
|
)
|
|
190
207
|
)
|
|
@@ -197,13 +214,25 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
197
214
|
"logging": {},
|
|
198
215
|
}
|
|
199
216
|
|
|
217
|
+
# Extract server-level icons and website_url
|
|
218
|
+
server_icons = (
|
|
219
|
+
[icon.model_dump() for icon in mcp._mcp_server.icons]
|
|
220
|
+
if hasattr(mcp._mcp_server, "icons") and mcp._mcp_server.icons
|
|
221
|
+
else None
|
|
222
|
+
)
|
|
223
|
+
server_website_url = (
|
|
224
|
+
mcp._mcp_server.website_url if hasattr(mcp._mcp_server, "website_url") else None
|
|
225
|
+
)
|
|
226
|
+
|
|
200
227
|
return FastMCPInfo(
|
|
201
228
|
name=mcp.name,
|
|
202
229
|
instructions=mcp.instructions,
|
|
230
|
+
version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
|
|
231
|
+
website_url=server_website_url,
|
|
232
|
+
icons=server_icons,
|
|
203
233
|
fastmcp_version=fastmcp.__version__,
|
|
204
234
|
mcp_version=importlib.metadata.version("mcp"),
|
|
205
235
|
server_generation=2, # FastMCP v2
|
|
206
|
-
version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
|
|
207
236
|
tools=tool_infos,
|
|
208
237
|
prompts=prompt_infos,
|
|
209
238
|
resources=resource_infos,
|
|
@@ -248,6 +277,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
248
277
|
tags=None, # v1 doesn't have tags
|
|
249
278
|
enabled=None, # v1 doesn't have enabled field
|
|
250
279
|
title=None, # v1 doesn't have title
|
|
280
|
+
icons=[icon.model_dump() for icon in mcp_tool.icons]
|
|
281
|
+
if hasattr(mcp_tool, "icons") and mcp_tool.icons
|
|
282
|
+
else None,
|
|
251
283
|
meta=None, # v1 doesn't have meta field
|
|
252
284
|
)
|
|
253
285
|
)
|
|
@@ -269,6 +301,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
269
301
|
tags=None, # v1 doesn't have tags
|
|
270
302
|
enabled=None, # v1 doesn't have enabled field
|
|
271
303
|
title=None, # v1 doesn't have title
|
|
304
|
+
icons=[icon.model_dump() for icon in mcp_prompt.icons]
|
|
305
|
+
if hasattr(mcp_prompt, "icons") and mcp_prompt.icons
|
|
306
|
+
else None,
|
|
272
307
|
meta=None, # v1 doesn't have meta field
|
|
273
308
|
)
|
|
274
309
|
)
|
|
@@ -287,6 +322,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
287
322
|
tags=None, # v1 doesn't have tags
|
|
288
323
|
enabled=None, # v1 doesn't have enabled field
|
|
289
324
|
title=None, # v1 doesn't have title
|
|
325
|
+
icons=[icon.model_dump() for icon in mcp_resource.icons]
|
|
326
|
+
if hasattr(mcp_resource, "icons") and mcp_resource.icons
|
|
327
|
+
else None,
|
|
290
328
|
meta=None, # v1 doesn't have meta field
|
|
291
329
|
)
|
|
292
330
|
)
|
|
@@ -306,6 +344,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
306
344
|
tags=None, # v1 doesn't have tags
|
|
307
345
|
enabled=None, # v1 doesn't have enabled field
|
|
308
346
|
title=None, # v1 doesn't have title
|
|
347
|
+
icons=[icon.model_dump() for icon in mcp_template.icons]
|
|
348
|
+
if hasattr(mcp_template, "icons") and mcp_template.icons
|
|
349
|
+
else None,
|
|
309
350
|
meta=None, # v1 doesn't have meta field
|
|
310
351
|
)
|
|
311
352
|
)
|
|
@@ -318,13 +359,26 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
318
359
|
"logging": {},
|
|
319
360
|
}
|
|
320
361
|
|
|
362
|
+
# Extract server-level icons and website_url from serverInfo
|
|
363
|
+
server_info = client.initialize_result.serverInfo
|
|
364
|
+
server_icons = (
|
|
365
|
+
[icon.model_dump() for icon in server_info.icons]
|
|
366
|
+
if hasattr(server_info, "icons") and server_info.icons
|
|
367
|
+
else None
|
|
368
|
+
)
|
|
369
|
+
server_website_url = (
|
|
370
|
+
server_info.websiteUrl if hasattr(server_info, "websiteUrl") else None
|
|
371
|
+
)
|
|
372
|
+
|
|
321
373
|
return FastMCPInfo(
|
|
322
374
|
name=mcp._mcp_server.name,
|
|
323
375
|
instructions=mcp._mcp_server.instructions,
|
|
376
|
+
version=mcp._mcp_server.version,
|
|
377
|
+
website_url=server_website_url,
|
|
378
|
+
icons=server_icons,
|
|
324
379
|
fastmcp_version=fastmcp.__version__, # Version generating this manifest
|
|
325
380
|
mcp_version=importlib.metadata.version("mcp"),
|
|
326
381
|
server_generation=1, # MCP v1
|
|
327
|
-
version=mcp._mcp_server.version,
|
|
328
382
|
tools=tool_infos,
|
|
329
383
|
prompts=prompt_infos,
|
|
330
384
|
resources=resource_infos,
|
|
@@ -369,6 +423,8 @@ async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
|
|
|
369
423
|
"name": info.name,
|
|
370
424
|
"instructions": info.instructions,
|
|
371
425
|
"version": info.version,
|
|
426
|
+
"website_url": info.website_url,
|
|
427
|
+
"icons": info.icons,
|
|
372
428
|
"generation": info.server_generation,
|
|
373
429
|
"capabilities": info.capabilities,
|
|
374
430
|
},
|