fastmcp 2.12.4__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.
Files changed (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1238 -234
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +12 -6
  28. fastmcp/server/auth/providers/aws.py +13 -2
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +13 -7
  32. fastmcp/server/auth/providers/google.py +13 -7
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +16 -13
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +53 -16
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +2 -2
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.4.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=".env",
90
+ env_file=ENV_FILE,
86
91
  extra="ignore",
87
92
  env_nested_delimiter="__",
88
93
  nested_model_default_partial_update=True,
@@ -189,7 +194,6 @@ class Settings(BaseSettings):
189
194
  client_raise_first_exceptiongroup_error: Annotated[
190
195
  bool,
191
196
  Field(
192
- default=True,
193
197
  description=inspect.cleandoc(
194
198
  """
195
199
  Many MCP components operate in anyio taskgroups, and raise
@@ -205,7 +209,6 @@ class Settings(BaseSettings):
205
209
  resource_prefix_format: Annotated[
206
210
  Literal["protocol", "path"],
207
211
  Field(
208
- default="path",
209
212
  description=inspect.cleandoc(
210
213
  """
211
214
  When perfixing a resource URI, either use path formatting (resource://prefix/path)
@@ -235,7 +238,6 @@ class Settings(BaseSettings):
235
238
  mask_error_details: Annotated[
236
239
  bool,
237
240
  Field(
238
- default=False,
239
241
  description=inspect.cleandoc(
240
242
  """
241
243
  If True, error details from user-supplied functions (tool, resource, prompt)
@@ -248,6 +250,22 @@ class Settings(BaseSettings):
248
250
  ),
249
251
  ] = False
250
252
 
253
+ strict_input_validation: Annotated[
254
+ bool,
255
+ Field(
256
+ description=inspect.cleandoc(
257
+ """
258
+ If True, tool inputs are strictly validated against the input
259
+ JSON schema. For example, providing the string \"10\" to an
260
+ integer field will raise an error. If False, compatible inputs
261
+ will be coerced to match the schema, which can increase
262
+ compatibility. For example, providing the string \"10\" to an
263
+ integer field will be coerced to 10. Defaults to False.
264
+ """
265
+ ),
266
+ ),
267
+ ] = False
268
+
251
269
  server_dependencies: list[str] = Field(
252
270
  default_factory=list,
253
271
  description="List of dependencies to install in the server environment",
@@ -293,7 +311,6 @@ class Settings(BaseSettings):
293
311
  include_tags: Annotated[
294
312
  set[str] | None,
295
313
  Field(
296
- default=None,
297
314
  description=inspect.cleandoc(
298
315
  """
299
316
  If provided, only components that match these tags will be
@@ -306,7 +323,6 @@ class Settings(BaseSettings):
306
323
  exclude_tags: Annotated[
307
324
  set[str] | None,
308
325
  Field(
309
- default=None,
310
326
  description=inspect.cleandoc(
311
327
  """
312
328
  If provided, components that match these tags will be excluded
@@ -320,7 +336,6 @@ class Settings(BaseSettings):
320
336
  include_fastmcp_meta: Annotated[
321
337
  bool,
322
338
  Field(
323
- default=True,
324
339
  description=inspect.cleandoc(
325
340
  """
326
341
  Whether to include FastMCP meta in the server's MCP responses.
@@ -335,7 +350,6 @@ class Settings(BaseSettings):
335
350
  mounted_components_raise_on_load_error: Annotated[
336
351
  bool,
337
352
  Field(
338
- default=False,
339
353
  description=inspect.cleandoc(
340
354
  """
341
355
  If True, errors encountered when loading mounted components (tools, resources, prompts)
@@ -349,7 +363,6 @@ class Settings(BaseSettings):
349
363
  show_cli_banner: Annotated[
350
364
  bool,
351
365
  Field(
352
- default=True,
353
366
  description=inspect.cleandoc(
354
367
  """
355
368
  If True, the server banner will be displayed when running the server via CLI.
fastmcp/tools/tool.py CHANGED
@@ -16,7 +16,7 @@ from typing import (
16
16
 
17
17
  import mcp.types
18
18
  import pydantic_core
19
- from mcp.types import ContentBlock, TextContent, ToolAnnotations
19
+ from mcp.types import ContentBlock, Icon, TextContent, ToolAnnotations
20
20
  from mcp.types import Tool as MCPTool
21
21
  from pydantic import Field, PydanticSchemaGenerationError
22
22
  from typing_extensions import TypeVar
@@ -156,6 +156,7 @@ class Tool(FastMCPComponent):
156
156
  description=overrides.get("description", self.description),
157
157
  inputSchema=overrides.get("inputSchema", self.parameters),
158
158
  outputSchema=overrides.get("outputSchema", self.output_schema),
159
+ icons=overrides.get("icons", self.icons),
159
160
  annotations=overrides.get("annotations", self.annotations),
160
161
  _meta=overrides.get(
161
162
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -168,6 +169,7 @@ class Tool(FastMCPComponent):
168
169
  name: str | None = None,
169
170
  title: str | None = None,
170
171
  description: str | None = None,
172
+ icons: list[Icon] | None = None,
171
173
  tags: set[str] | None = None,
172
174
  annotations: ToolAnnotations | None = None,
173
175
  exclude_args: list[str] | None = None,
@@ -182,6 +184,7 @@ class Tool(FastMCPComponent):
182
184
  name=name,
183
185
  title=title,
184
186
  description=description,
187
+ icons=icons,
185
188
  tags=tags,
186
189
  annotations=annotations,
187
190
  exclude_args=exclude_args,
@@ -248,6 +251,7 @@ class FunctionTool(Tool):
248
251
  name: str | None = None,
249
252
  title: str | None = None,
250
253
  description: str | None = None,
254
+ icons: list[Icon] | None = None,
251
255
  tags: set[str] | None = None,
252
256
  annotations: ToolAnnotations | None = None,
253
257
  exclude_args: list[str] | None = None,
@@ -291,6 +295,7 @@ class FunctionTool(Tool):
291
295
  name=name or parsed_fn.name,
292
296
  title=title,
293
297
  description=description or parsed_fn.description,
298
+ icons=icons,
294
299
  parameters=parsed_fn.input_schema,
295
300
  output_schema=final_output_schema,
296
301
  annotations=annotations,
@@ -546,13 +551,12 @@ def _convert_to_content(
546
551
 
547
552
  # If any item is a ContentBlock, convert non-ContentBlock items to TextContent
548
553
  # without aggregating them
549
- if any(isinstance(item, ContentBlock) for item in result):
554
+ if any(isinstance(item, ContentBlock | Image | Audio | File) for item in result):
550
555
  return [
551
556
  _convert_to_single_content_block(item, serializer)
552
557
  if not isinstance(item, ContentBlock)
553
558
  else item
554
559
  for item in result
555
560
  ]
556
-
557
561
  # If none of the items are ContentBlocks, aggregate all items into a single TextContent
558
562
  return [TextContent(type="text", text=_serialize_with_fallback(result, serializer))]
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import warnings
4
4
  from collections.abc import Callable
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import Any
6
6
 
7
7
  from mcp.types import ToolAnnotations
8
+ from pydantic import ValidationError
8
9
 
9
10
  from fastmcp import settings
10
11
  from fastmcp.exceptions import NotFoundError, ToolError
@@ -16,9 +17,6 @@ from fastmcp.tools.tool_transform import (
16
17
  )
17
18
  from fastmcp.utilities.logging import get_logger
18
19
 
19
- if TYPE_CHECKING:
20
- from fastmcp.server.server import MountedServer
21
-
22
20
  logger = get_logger(__name__)
23
21
 
24
22
 
@@ -32,7 +30,6 @@ class ToolManager:
32
30
  transformations: dict[str, ToolTransformConfig] | None = None,
33
31
  ):
34
32
  self._tools: dict[str, Tool] = {}
35
- self._mounted_servers: list[MountedServer] = []
36
33
  self.mask_error_details = mask_error_details or settings.mask_error_details
37
34
  self.transformations = transformations or {}
38
35
 
@@ -48,56 +45,12 @@ class ToolManager:
48
45
 
49
46
  self.duplicate_behavior = duplicate_behavior
50
47
 
51
- def mount(self, server: MountedServer) -> None:
52
- """Adds a mounted server as a source for tools."""
53
- self._mounted_servers.append(server)
54
-
55
- async def _load_tools(self, *, via_server: bool = False) -> dict[str, Tool]:
56
- """
57
- The single, consolidated recursive method for fetching tools. The 'via_server'
58
- parameter determines the communication path.
59
-
60
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
61
- - via_server=True: Server-to-server path for filtered MCP requests
62
- """
63
- all_tools: dict[str, Tool] = {}
64
-
65
- for mounted in self._mounted_servers:
66
- try:
67
- if via_server:
68
- # Use the server-to-server filtered path
69
- child_results = await mounted.server._list_tools()
70
- else:
71
- # Use the manager-to-manager unfiltered path
72
- child_results = await mounted.server._tool_manager.list_tools()
73
-
74
- # The combination logic is the same for both paths
75
- child_dict = {t.key: t for t in child_results}
76
- if mounted.prefix:
77
- for tool in child_dict.values():
78
- prefixed_tool = tool.model_copy(
79
- key=f"{mounted.prefix}_{tool.key}"
80
- )
81
- all_tools[prefixed_tool.key] = prefixed_tool
82
- else:
83
- all_tools.update(child_dict)
84
- except Exception as e:
85
- # Skip failed mounts silently, matches existing behavior
86
- logger.warning(
87
- f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
88
- )
89
- if settings.mounted_components_raise_on_load_error:
90
- raise
91
- continue
92
-
93
- # Finally, add local tools, which always take precedence
94
- all_tools.update(self._tools)
95
-
48
+ async def _load_tools(self) -> dict[str, Tool]:
49
+ """Return this manager's local tools with transformations applied."""
96
50
  transformed_tools = apply_transformations_to_tools(
97
- tools=all_tools,
51
+ tools=self._tools,
98
52
  transformations=self.transformations,
99
53
  )
100
-
101
54
  return transformed_tools
102
55
 
103
56
  async def has_tool(self, key: str) -> bool:
@@ -114,25 +67,9 @@ class ToolManager:
114
67
 
115
68
  async def get_tools(self) -> dict[str, Tool]:
116
69
  """
117
- Gets the complete, unfiltered inventory of all tools.
118
- """
119
- return await self._load_tools(via_server=False)
120
-
121
- async def list_tools(self) -> list[Tool]:
122
- """
123
- Lists all tools, applying protocol filtering.
70
+ Gets the complete, unfiltered inventory of local tools.
124
71
  """
125
- tools_dict = await self._load_tools(via_server=True)
126
- return list(tools_dict.values())
127
-
128
- @property
129
- def _tools_transformed(self) -> list[str]:
130
- """Get the local tools."""
131
-
132
- return [
133
- transformation.name or tool_name
134
- for tool_name, transformation in self.transformations.items()
135
- ]
72
+ return await self._load_tools()
136
73
 
137
74
  def add_tool_from_fn(
138
75
  self,
@@ -214,41 +151,18 @@ class ToolManager:
214
151
  Internal API for servers: Finds and calls a tool, respecting the
215
152
  filtered protocol path.
216
153
  """
217
- # 1. Check local tools first. The server will have already applied its filter.
218
- if key in self._tools or key in self._tools_transformed:
219
- tool = await self.get_tool(key)
220
- if not tool:
221
- raise NotFoundError(f"Tool {key!r} not found")
222
-
223
- try:
224
- return await tool.run(arguments)
225
-
226
- # raise ToolErrors as-is
227
- except ToolError as e:
228
- logger.exception(f"Error calling tool {key!r}")
229
- raise e
230
-
231
- # Handle other exceptions
232
- except Exception as e:
233
- logger.exception(f"Error calling tool {key!r}")
234
- if self.mask_error_details:
235
- # Mask internal details
236
- raise ToolError(f"Error calling tool {key!r}") from e
237
- else:
238
- # Include original error details
239
- raise ToolError(f"Error calling tool {key!r}: {e}") from e
240
-
241
- # 2. Check mounted servers using the filtered protocol path.
242
- for mounted in reversed(self._mounted_servers):
243
- tool_key = key
244
- if mounted.prefix:
245
- if key.startswith(f"{mounted.prefix}_"):
246
- tool_key = key.removeprefix(f"{mounted.prefix}_")
247
- else:
248
- continue
249
- try:
250
- return await mounted.server._call_tool(tool_key, arguments)
251
- except NotFoundError:
252
- continue
253
-
254
- raise NotFoundError(f"Tool {key!r} not found.")
154
+ tool = await self.get_tool(key)
155
+ try:
156
+ return await tool.run(arguments)
157
+ except ValidationError as e:
158
+ logger.exception(f"Error validating tool {key!r}: {e}")
159
+ raise e
160
+ except ToolError as e:
161
+ logger.exception(f"Error calling tool {key!r}")
162
+ raise e
163
+ except Exception as e:
164
+ logger.exception(f"Error calling tool {key!r}")
165
+ if self.mask_error_details:
166
+ raise ToolError(f"Error calling tool {key!r}") from e
167
+ else:
168
+ raise ToolError(f"Error calling tool {key!r}: {e}") from e
@@ -34,7 +34,7 @@ _current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[
34
34
  )
35
35
 
36
36
 
37
- async def forward(**kwargs) -> ToolResult:
37
+ async def forward(**kwargs: Any) -> ToolResult:
38
38
  """Forward to parent tool with argument transformation applied.
39
39
 
40
40
  This function can only be called from within a transformed tool's custom
@@ -64,7 +64,7 @@ async def forward(**kwargs) -> ToolResult:
64
64
  return await tool.forwarding_fn(**kwargs)
65
65
 
66
66
 
67
- async def forward_raw(**kwargs) -> ToolResult:
67
+ async def forward_raw(**kwargs: Any) -> ToolResult:
68
68
  """Forward directly to parent tool without transformation.
69
69
 
70
70
  This function bypasses all argument transformation and validation, calling the parent
@@ -681,7 +681,7 @@ class TransformedTool(Tool):
681
681
  schema = compress_schema(schema, prune_defs=True)
682
682
 
683
683
  # Create forwarding function that closes over everything it needs
684
- async def _forward(**kwargs):
684
+ async def _forward(**kwargs: Any):
685
685
  # Validate arguments
686
686
  valid_args = set(new_props.keys())
687
687
  provided_args = set(kwargs.keys())
fastmcp/utilities/cli.py CHANGED
@@ -186,7 +186,7 @@ def log_server_banner(
186
186
  case "stdio":
187
187
  display_transport = "STDIO"
188
188
 
189
- info_table.add_row("🖥️", "Server name:", server.name)
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
  )
@@ -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.",
@@ -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 the components using FastMCP2's direct methods
108
- tools_dict = await mcp.get_tools()
109
- prompts_dict = await mcp.get_prompts()
110
- resources_dict = await mcp.get_resources()
111
- templates_dict = await mcp.get_resource_templates()
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 key, tool in tools_dict.items():
116
- # Convert to MCP tool to get input schema
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 key, prompt in prompts_dict.items():
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 key, resource in resources_dict.items():
164
+ for resource in resources_list:
154
165
  resource_infos.append(
155
166
  ResourceInfo(
156
- key=key,
157
- uri=key, # For v2, key is the URI
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 key, template in templates_dict.items():
187
+ for template in templates_list:
174
188
  template_infos.append(
175
189
  TemplateInfo(
176
- key=key,
177
- uri_template=key, # For v2, key is the URI template
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
  },