diff --git a/docs/docs.json b/docs/docs.json
index da16b3cf3..8dedddcdf 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -104,6 +104,7 @@
"group": "Advanced Features",
"icon": "stars",
"pages": [
+ "servers/icons",
"servers/context",
"servers/proxy",
"servers/composition",
diff --git a/docs/servers/icons.mdx b/docs/servers/icons.mdx
new file mode 100644
index 000000000..5dfd828fa
--- /dev/null
+++ b/docs/servers/icons.mdx
@@ -0,0 +1,128 @@
+---
+title: Icons
+description: Add visual icons to your servers, tools, resources, and prompts
+icon: image
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Icons provide visual representations for your MCP servers and components, helping client applications present better user interfaces. When displayed in MCP clients, icons help users quickly identify and navigate your server's capabilities.
+
+## Icon Format
+
+Icons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies:
+
+- **src**: URL or data URI pointing to the icon image
+- **mimeType** (optional): MIME type of the image (e.g., "image/png", "image/svg+xml")
+- **sizes** (optional): Array of size descriptors (e.g., ["48x48"], ["any"])
+
+```python
+from mcp.types import Icon
+
+icon = Icon(
+ src="https://example.com/icon.png",
+ mimeType="image/png",
+ sizes=["48x48"]
+)
+```
+
+## Server Icons
+
+Add icons and a website URL to your server for display in client applications:
+
+```python
+from fastmcp import FastMCP
+from mcp.types import Icon
+
+mcp = FastMCP(
+ name="WeatherService",
+ website_url="https://weather.example.com",
+ icons=[
+ Icon(
+ src="https://weather.example.com/icon-48.png",
+ mimeType="image/png",
+ sizes=["48x48"]
+ ),
+ Icon(
+ src="https://weather.example.com/icon-96.png",
+ mimeType="image/png",
+ sizes=["96x96"]
+ ),
+ ]
+)
+```
+
+Server icons appear in MCP client interfaces to help users identify your server among others they may have installed.
+
+## Component Icons
+
+Icons can be added to individual tools, resources, resource templates, and prompts:
+
+### Tool Icons
+
+```python
+from mcp.types import Icon
+
+@mcp.tool(
+ icons=[Icon(src="https://example.com/calculator-icon.png")]
+)
+def calculate_sum(a: int, b: int) -> int:
+ """Add two numbers together."""
+ return a + b
+```
+
+### Resource Icons
+
+```python
+@mcp.resource(
+ "config://settings",
+ icons=[Icon(src="https://example.com/config-icon.png")]
+)
+def get_settings() -> dict:
+ """Retrieve application settings."""
+ return {"theme": "dark", "language": "en"}
+```
+
+### Resource Template Icons
+
+```python
+@mcp.resource(
+ "user://{user_id}/profile",
+ icons=[Icon(src="https://example.com/user-icon.png")]
+)
+def get_user_profile(user_id: str) -> dict:
+ """Get a user's profile."""
+ return {"id": user_id, "name": f"User {user_id}"}
+```
+
+### Prompt Icons
+
+```python
+@mcp.prompt(
+ icons=[Icon(src="https://example.com/prompt-icon.png")]
+)
+def analyze_code(code: str):
+ """Create a prompt for code analysis."""
+ return f"Please analyze this code:\n\n{code}"
+```
+
+## Using Data URIs
+
+For small icons or when you want to embed the icon directly, use data URIs:
+
+```python
+from mcp.types import Icon
+
+# SVG icon as data URI
+svg_icon = Icon(
+ src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+",
+ mimeType="image/svg+xml"
+)
+
+@mcp.tool(icons=[svg_icon])
+def my_tool() -> str:
+ """A tool with an embedded SVG icon."""
+ return "result"
+```
diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx
index 24ebcfa29..c122daa2b 100644
--- a/docs/servers/prompts.mdx
+++ b/docs/servers/prompts.mdx
@@ -97,9 +97,15 @@ def data_analysis_prompt(
A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information
+
+
+
+ Optional list of icon representations for this prompt. See [Icons](/servers/icons) for detailed examples
+
+
-
+
Optional meta information about the prompt. This data is passed through to the MCP client as the `_meta` field of the client-side prompt object and can be used for custom metadata, versioning, or other application-specific purposes.
diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx
index 60d03a6c4..d3c8e3cdd 100644
--- a/docs/servers/resources.mdx
+++ b/docs/servers/resources.mdx
@@ -106,6 +106,12 @@ def get_application_status() -> dict:
A boolean to enable or disable the resource. See [Disabling Resources](#disabling-resources) for more information
+
+
+
+ Optional list of icon representations for this resource or template. See [Icons](/servers/icons) for detailed examples
+
+
An optional `Annotations` object or dictionary to add additional metadata about the resource.
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index 982dc25ff..8c46654a0 100644
--- a/docs/servers/server.mdx
+++ b/docs/servers/server.mdx
@@ -40,6 +40,22 @@ The `FastMCP` constructor accepts several arguments:
Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality
+
+ Version string for your server. If not provided, defaults to the FastMCP library version
+
+
+
+
+
+ URL to a website with more information about your server. Displayed in client applications
+
+
+
+
+
+ List of icon representations for your server. Icons help users visually identify your server in client applications. See [Icons](/servers/icons) for detailed examples
+
+
Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration options
diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx
index 380fe5a7f..e9ee1af77 100644
--- a/docs/servers/tools.mdx
+++ b/docs/servers/tools.mdx
@@ -81,6 +81,12 @@ def search_products_implementation(query: str, category: str | None = None) -> l
A boolean to enable or disable the tool. See [Disabling Tools](#disabling-tools) for more information
+
+
+
+ Optional list of icon representations for this tool. See [Icons](/servers/icons) for detailed examples
+
+
A list of argument names to exclude from the tool schema shown to the LLM. See [Excluding Arguments](#excluding-arguments) for more information
diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py
index 42f582b1c..1cd159680 100644
--- a/src/fastmcp/cli/cli.py
+++ b/src/fastmcp/cli/cli.py
@@ -713,6 +713,10 @@ async def inspect(
console.print(f" Name: {info.name}")
if info.version:
console.print(f" Version: {info.version}")
+ if info.website_url:
+ console.print(f" Website: {info.website_url}")
+ if info.icons:
+ console.print(f" Icons: {len(info.icons)}")
console.print(f" Generation: {info.server_generation}")
if info.instructions:
console.print(f" Instructions: {info.instructions}")
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index 2785d04df..a0e1bff31 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Sequence
from typing import Any
import pydantic_core
-from mcp.types import ContentBlock, PromptMessage, Role, TextContent
+from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
from mcp.types import Prompt as MCPPrompt
from mcp.types import PromptArgument as MCPPromptArgument
from pydantic import Field, TypeAdapter
@@ -105,6 +105,7 @@ class Prompt(FastMCPComponent):
description=overrides.get("description", self.description),
arguments=arguments,
title=overrides.get("title", self.title),
+ icons=overrides.get("icons", self.icons),
_meta=overrides.get(
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
),
@@ -116,6 +117,7 @@ class Prompt(FastMCPComponent):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
meta: dict[str, Any] | None = None,
@@ -133,6 +135,7 @@ class Prompt(FastMCPComponent):
name=name,
title=title,
description=description,
+ icons=icons,
tags=tags,
enabled=enabled,
meta=meta,
@@ -162,6 +165,7 @@ class FunctionPrompt(Prompt):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
meta: dict[str, Any] | None = None,
@@ -255,6 +259,7 @@ class FunctionPrompt(Prompt):
name=func_name,
title=title,
description=description,
+ icons=icons,
arguments=arguments,
tags=tags or set(),
enabled=enabled if enabled is not None else True,
diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py
index d7f9d7177..90de857a0 100644
--- a/src/fastmcp/resources/resource.py
+++ b/src/fastmcp/resources/resource.py
@@ -7,7 +7,7 @@ from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any
import pydantic_core
-from mcp.types import Annotations
+from mcp.types import Annotations, Icon
from mcp.types import Resource as MCPResource
from pydantic import (
AnyUrl,
@@ -72,6 +72,7 @@ class Resource(FastMCPComponent):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@@ -84,6 +85,7 @@ class Resource(FastMCPComponent):
name=name,
title=title,
description=description,
+ icons=icons,
mime_type=mime_type,
tags=tags,
enabled=enabled,
@@ -132,6 +134,7 @@ class Resource(FastMCPComponent):
description=overrides.get("description", self.description),
mimeType=overrides.get("mimeType", self.mime_type),
title=overrides.get("title", self.title),
+ icons=overrides.get("icons", self.icons),
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get(
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -175,6 +178,7 @@ class FunctionResource(Resource):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@@ -190,6 +194,7 @@ class FunctionResource(Resource):
name=name or get_fn_name(fn),
title=title,
description=description or inspect.getdoc(fn),
+ icons=icons,
mime_type=mime_type or "text/plain",
tags=tags or set(),
enabled=enabled if enabled is not None else True,
diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py
index a604b124a..cde1d273c 100644
--- a/src/fastmcp/resources/template.py
+++ b/src/fastmcp/resources/template.py
@@ -8,7 +8,7 @@ from collections.abc import Callable
from typing import Any
from urllib.parse import parse_qs, unquote
-from mcp.types import Annotations
+from mcp.types import Annotations, Icon
from mcp.types import ResourceTemplate as MCPResourceTemplate
from pydantic import (
Field,
@@ -133,6 +133,7 @@ class ResourceTemplate(FastMCPComponent):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@@ -145,6 +146,7 @@ class ResourceTemplate(FastMCPComponent):
name=name,
title=title,
description=description,
+ icons=icons,
mime_type=mime_type,
tags=tags,
enabled=enabled,
@@ -202,6 +204,7 @@ class ResourceTemplate(FastMCPComponent):
description=overrides.get("description", self.description),
mimeType=overrides.get("mimeType", self.mime_type),
title=overrides.get("title", self.title),
+ icons=overrides.get("icons", self.icons),
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get(
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -285,6 +288,7 @@ class FunctionResourceTemplate(ResourceTemplate):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@@ -388,6 +392,7 @@ class FunctionResourceTemplate(ResourceTemplate):
name=func_name,
title=title,
description=description,
+ icons=icons,
mime_type=mime_type or "text/plain",
fn=fn,
parameters=parameters,
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 6c91e870e..813831ba3 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -138,6 +138,8 @@ class FastMCP(Generic[LifespanResultT]):
instructions: str | None = None,
*,
version: str | None = None,
+ website_url: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
auth: AuthProvider | None | NotSetT = NotSet,
middleware: list[Middleware] | None = None,
lifespan: LifespanCallable | None = None,
@@ -202,6 +204,8 @@ class FastMCP(Generic[LifespanResultT]):
name=name or self.generate_name(),
version=version or fastmcp.__version__,
instructions=instructions,
+ website_url=website_url,
+ icons=icons,
lifespan=_lifespan_proxy(fastmcp_server=self),
)
@@ -1331,6 +1335,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -1347,6 +1352,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -1362,6 +1368,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
@@ -1445,6 +1452,7 @@ class FastMCP(Generic[LifespanResultT]):
name=tool_name,
title=title,
description=description,
+ icons=icons,
tags=tags,
output_schema=output_schema,
annotations=cast(ToolAnnotations | None, annotations),
@@ -1478,6 +1486,7 @@ class FastMCP(Generic[LifespanResultT]):
name=tool_name,
title=title,
description=description,
+ icons=icons,
tags=tags,
output_schema=output_schema,
annotations=annotations,
@@ -1575,6 +1584,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@@ -1674,6 +1684,7 @@ class FastMCP(Generic[LifespanResultT]):
name=name,
title=title,
description=description,
+ icons=icons,
mime_type=mime_type,
tags=tags,
enabled=enabled,
@@ -1689,6 +1700,7 @@ class FastMCP(Generic[LifespanResultT]):
name=name,
title=title,
description=description,
+ icons=icons,
mime_type=mime_type,
tags=tags,
enabled=enabled,
@@ -1735,6 +1747,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
meta: dict[str, Any] | None = None,
@@ -1748,6 +1761,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
meta: dict[str, Any] | None = None,
@@ -1760,6 +1774,7 @@ class FastMCP(Generic[LifespanResultT]):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[mcp.types.Icon] | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
meta: dict[str, Any] | None = None,
@@ -1859,6 +1874,7 @@ class FastMCP(Generic[LifespanResultT]):
name=prompt_name,
title=title,
description=description,
+ icons=icons,
tags=tags,
enabled=enabled,
meta=meta,
@@ -1889,6 +1905,7 @@ class FastMCP(Generic[LifespanResultT]):
name=prompt_name,
title=title,
description=description,
+ icons=icons,
tags=tags,
enabled=enabled,
meta=meta,
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index d1fd03f64..b3bb67398 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -16,7 +16,7 @@ from typing import (
import mcp.types
import pydantic_core
-from mcp.types import ContentBlock, TextContent, ToolAnnotations
+from mcp.types import ContentBlock, Icon, TextContent, ToolAnnotations
from mcp.types import Tool as MCPTool
from pydantic import Field, PydanticSchemaGenerationError
from typing_extensions import TypeVar
@@ -156,6 +156,7 @@ class Tool(FastMCPComponent):
description=overrides.get("description", self.description),
inputSchema=overrides.get("inputSchema", self.parameters),
outputSchema=overrides.get("outputSchema", self.output_schema),
+ icons=overrides.get("icons", self.icons),
annotations=overrides.get("annotations", self.annotations),
_meta=overrides.get(
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -168,6 +169,7 @@ class Tool(FastMCPComponent):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | None = None,
exclude_args: list[str] | None = None,
@@ -182,6 +184,7 @@ class Tool(FastMCPComponent):
name=name,
title=title,
description=description,
+ icons=icons,
tags=tags,
annotations=annotations,
exclude_args=exclude_args,
@@ -248,6 +251,7 @@ class FunctionTool(Tool):
name: str | None = None,
title: str | None = None,
description: str | None = None,
+ icons: list[Icon] | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | None = None,
exclude_args: list[str] | None = None,
@@ -291,6 +295,7 @@ class FunctionTool(Tool):
name=name or parsed_fn.name,
title=title,
description=description or parsed_fn.description,
+ icons=icons,
parameters=parsed_fn.input_schema,
output_schema=final_output_schema,
annotations=annotations,
diff --git a/src/fastmcp/utilities/components.py b/src/fastmcp/utilities/components.py
index 79b98cbf1..91e0777b0 100644
--- a/src/fastmcp/utilities/components.py
+++ b/src/fastmcp/utilities/components.py
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Sequence
from typing import Annotated, Any, TypedDict
+from mcp.types import Icon
from pydantic import BeforeValidator, Field, PrivateAttr
from typing_extensions import Self, TypeVar
@@ -39,6 +40,10 @@ class FastMCPComponent(FastMCPBaseModel):
default=None,
description="The description of the component.",
)
+ icons: list[Icon] | None = Field(
+ default=None,
+ description="Optional list of icons for this component to display in user interfaces.",
+ )
tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(
default_factory=set,
description="Tags for the component.",
diff --git a/src/fastmcp/utilities/inspect.py b/src/fastmcp/utilities/inspect.py
index f68921674..047a9b55c 100644
--- a/src/fastmcp/utilities/inspect.py
+++ b/src/fastmcp/utilities/inspect.py
@@ -28,6 +28,7 @@ class ToolInfo:
tags: list[str] | None = None
enabled: bool | None = None
title: str | None = None
+ icons: list[dict[str, Any]] | None = None
meta: dict[str, Any] | None = None
@@ -42,6 +43,7 @@ class PromptInfo:
tags: list[str] | None = None
enabled: bool | None = None
title: str | None = None
+ icons: list[dict[str, Any]] | None = None
meta: dict[str, Any] | None = None
@@ -58,6 +60,7 @@ class ResourceInfo:
tags: list[str] | None = None
enabled: bool | None = None
title: str | None = None
+ icons: list[dict[str, Any]] | None = None
meta: dict[str, Any] | None = None
@@ -75,6 +78,7 @@ class TemplateInfo:
tags: list[str] | None = None
enabled: bool | None = None
title: str | None = None
+ icons: list[dict[str, Any]] | None = None
meta: dict[str, Any] | None = None
@@ -85,6 +89,8 @@ class FastMCPInfo:
name: str
instructions: str | None
version: str | None # The server's own version string (if specified)
+ website_url: str | None
+ icons: list[dict[str, Any]] | None
fastmcp_version: str # Version of FastMCP generating this manifest
mcp_version: str # Version of MCP protocol library
server_generation: int # Server generation: 1 (mcp package) or 2 (fastmcp)
@@ -125,6 +131,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
tags=list(tool.tags) if tool.tags else None,
enabled=tool.enabled,
title=tool.title,
+ icons=[icon.model_dump() for icon in tool.icons]
+ if tool.icons
+ else None,
meta=tool.meta,
)
)
@@ -143,6 +152,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
tags=list(prompt.tags) if prompt.tags else None,
enabled=prompt.enabled,
title=prompt.title,
+ icons=[icon.model_dump() for icon in prompt.icons]
+ if prompt.icons
+ else None,
meta=prompt.meta,
)
)
@@ -163,6 +175,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
tags=list(resource.tags) if resource.tags else None,
enabled=resource.enabled,
title=resource.title,
+ icons=[icon.model_dump() for icon in resource.icons]
+ if resource.icons
+ else None,
meta=resource.meta,
)
)
@@ -184,6 +199,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
tags=list(template.tags) if template.tags else None,
enabled=template.enabled,
title=template.title,
+ icons=[icon.model_dump() for icon in template.icons]
+ if template.icons
+ else None,
meta=template.meta,
)
)
@@ -196,13 +214,25 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
"logging": {},
}
+ # Extract server-level icons and website_url
+ server_icons = (
+ [icon.model_dump() for icon in mcp._mcp_server.icons]
+ if hasattr(mcp._mcp_server, "icons") and mcp._mcp_server.icons
+ else None
+ )
+ server_website_url = (
+ mcp._mcp_server.website_url if hasattr(mcp._mcp_server, "website_url") else None
+ )
+
return FastMCPInfo(
name=mcp.name,
instructions=mcp.instructions,
+ version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
+ website_url=server_website_url,
+ icons=server_icons,
fastmcp_version=fastmcp.__version__,
mcp_version=importlib.metadata.version("mcp"),
server_generation=2, # FastMCP v2
- version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
tools=tool_infos,
prompts=prompt_infos,
resources=resource_infos,
@@ -247,6 +277,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
tags=None, # v1 doesn't have tags
enabled=None, # v1 doesn't have enabled field
title=None, # v1 doesn't have title
+ icons=[icon.model_dump() for icon in mcp_tool.icons]
+ if hasattr(mcp_tool, "icons") and mcp_tool.icons
+ else None,
meta=None, # v1 doesn't have meta field
)
)
@@ -268,6 +301,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
tags=None, # v1 doesn't have tags
enabled=None, # v1 doesn't have enabled field
title=None, # v1 doesn't have title
+ icons=[icon.model_dump() for icon in mcp_prompt.icons]
+ if hasattr(mcp_prompt, "icons") and mcp_prompt.icons
+ else None,
meta=None, # v1 doesn't have meta field
)
)
@@ -286,6 +322,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
tags=None, # v1 doesn't have tags
enabled=None, # v1 doesn't have enabled field
title=None, # v1 doesn't have title
+ icons=[icon.model_dump() for icon in mcp_resource.icons]
+ if hasattr(mcp_resource, "icons") and mcp_resource.icons
+ else None,
meta=None, # v1 doesn't have meta field
)
)
@@ -305,6 +344,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
tags=None, # v1 doesn't have tags
enabled=None, # v1 doesn't have enabled field
title=None, # v1 doesn't have title
+ icons=[icon.model_dump() for icon in mcp_template.icons]
+ if hasattr(mcp_template, "icons") and mcp_template.icons
+ else None,
meta=None, # v1 doesn't have meta field
)
)
@@ -317,13 +359,26 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
"logging": {},
}
+ # Extract server-level icons and website_url from serverInfo
+ server_info = client.initialize_result.serverInfo
+ server_icons = (
+ [icon.model_dump() for icon in server_info.icons]
+ if hasattr(server_info, "icons") and server_info.icons
+ else None
+ )
+ server_website_url = (
+ server_info.websiteUrl if hasattr(server_info, "websiteUrl") else None
+ )
+
return FastMCPInfo(
name=mcp._mcp_server.name,
instructions=mcp._mcp_server.instructions,
+ version=mcp._mcp_server.version,
+ website_url=server_website_url,
+ icons=server_icons,
fastmcp_version=fastmcp.__version__, # Version generating this manifest
mcp_version=importlib.metadata.version("mcp"),
server_generation=1, # MCP v1
- version=mcp._mcp_server.version,
tools=tool_infos,
prompts=prompt_infos,
resources=resource_infos,
@@ -368,6 +423,8 @@ async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
"name": info.name,
"instructions": info.instructions,
"version": info.version,
+ "website_url": info.website_url,
+ "icons": info.icons,
"generation": info.server_generation,
"capabilities": info.capabilities,
},
diff --git a/tests/server/middleware/test_logging.py b/tests/server/middleware/test_logging.py
index f63165a66..eee7dae48 100644
--- a/tests/server/middleware/test_logging.py
+++ b/tests/server/middleware/test_logging.py
@@ -332,7 +332,7 @@ class TestLoggingMiddleware:
assert get_log_lines(caplog) == snapshot(
[
- '{"event": "request_start", "method": "test_method", "source": "client", "payload": "{\\"name\\":\\"tmpl\\",\\"title\\":null,\\"description\\":null,\\"tags\\":[],\\"meta\\":null,\\"enabled\\":true,\\"uri_template\\":\\"tmpl://{id}\\",\\"mime_type\\":\\"text/plain\\",\\"parameters\\":{\\"id\\":{\\"type\\":\\"string\\"}},\\"annotations\\":null}", "payload_type": "ResourceTemplate"}',
+ '{"event": "request_start", "method": "test_method", "source": "client", "payload": "{\\"name\\":\\"tmpl\\",\\"title\\":null,\\"description\\":null,\\"icons\\":null,\\"tags\\":[],\\"meta\\":null,\\"enabled\\":true,\\"uri_template\\":\\"tmpl://{id}\\",\\"mime_type\\":\\"text/plain\\",\\"parameters\\":{\\"id\\":{\\"type\\":\\"string\\"}},\\"annotations\\":null}", "payload_type": "ResourceTemplate"}',
'{"event": "request_success", "method": "test_method", "source": "client", "duration_ms": 0.02}',
]
)
diff --git a/tests/server/test_icons.py b/tests/server/test_icons.py
new file mode 100644
index 000000000..a937728fa
--- /dev/null
+++ b/tests/server/test_icons.py
@@ -0,0 +1,342 @@
+"""Tests for icon support across all MCP object types."""
+
+from mcp.types import Icon
+
+from fastmcp import Client, FastMCP
+from fastmcp.prompts import Message, Prompt
+from fastmcp.resources import Resource
+from fastmcp.resources.template import ResourceTemplate
+from fastmcp.tools import Tool
+
+
+class TestServerIcons:
+ """Test icon support at the server/implementation level."""
+
+ async def test_server_with_icons_and_website_url(self):
+ """Test that server accepts icons and websiteUrl in constructor."""
+ icons = [
+ Icon(
+ src="https://example.com/icon.png",
+ mimeType="image/png",
+ sizes=["48x48"],
+ ),
+ Icon(
+ src="data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=",
+ mimeType="image/svg+xml",
+ sizes=["any"],
+ ),
+ ]
+
+ mcp = FastMCP(
+ name="TestServer",
+ version="1.0.0",
+ website_url="https://example.com",
+ icons=icons,
+ )
+
+ # Verify that icons and website_url are passed to the underlying server
+ async with Client(mcp) as client:
+ server_info = client.initialize_result.serverInfo
+ assert server_info.websiteUrl == "https://example.com"
+ assert server_info.icons == icons
+
+ async def test_server_without_icons_and_website_url(self):
+ """Test that server works without icons and websiteUrl."""
+ mcp = FastMCP(name="TestServer")
+
+ async with Client(mcp) as client:
+ server_info = client.initialize_result.serverInfo
+ assert server_info.websiteUrl is None
+ assert server_info.icons is None
+
+
+class TestToolIcons:
+ """Test icon support for tools."""
+
+ async def test_tool_with_icons(self):
+ """Test that tools can have icons."""
+ mcp = FastMCP("TestServer")
+
+ icons = [
+ Icon(src="https://example.com/tool-icon.png", mimeType="image/png"),
+ ]
+
+ @mcp.tool(icons=icons)
+ def my_tool(name: str) -> str:
+ """A tool with an icon."""
+ return f"Hello, {name}!"
+
+ async with Client(mcp) as client:
+ tools = await client.list_tools()
+ assert len(tools) == 1
+ tool = tools[0]
+ assert tool.icons == icons
+
+ async def test_tool_from_function_with_icons(self):
+ """Test creating a tool from a function with icons."""
+ icons = [Icon(src="https://example.com/icon.png")]
+
+ def my_function(x: int) -> int:
+ """A function."""
+ return x * 2
+
+ tool = Tool.from_function(my_function, icons=icons)
+ assert tool.icons == icons
+
+ # Verify it converts to MCP tool correctly
+ mcp_tool = tool.to_mcp_tool()
+ assert mcp_tool.icons == icons
+
+ async def test_tool_without_icons(self):
+ """Test that tools work without icons."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.tool
+ def my_tool(name: str) -> str:
+ """A tool without an icon."""
+ return f"Hello, {name}!"
+
+ async with Client(mcp) as client:
+ tools = await client.list_tools()
+ assert len(tools) == 1
+ tool = tools[0]
+ assert tool.icons is None
+
+
+class TestResourceIcons:
+ """Test icon support for resources."""
+
+ async def test_resource_with_icons(self):
+ """Test that resources can have icons."""
+ mcp = FastMCP("TestServer")
+
+ icons = [Icon(src="https://example.com/resource-icon.png")]
+
+ @mcp.resource("test://resource", icons=icons)
+ def my_resource() -> str:
+ """A resource with an icon."""
+ return "Resource content"
+
+ async with Client(mcp) as client:
+ resources = await client.list_resources()
+ assert len(resources) == 1
+ resource = resources[0]
+ assert resource.icons == icons
+
+ async def test_resource_from_function_with_icons(self):
+ """Test creating a resource from a function with icons."""
+ icons = [Icon(src="https://example.com/icon.png")]
+
+ def my_function() -> str:
+ """A function."""
+ return "content"
+
+ resource = Resource.from_function(
+ my_function,
+ uri="test://resource",
+ icons=icons,
+ )
+ assert resource.icons == icons
+
+ # Verify it converts to MCP resource correctly
+ mcp_resource = resource.to_mcp_resource()
+ assert mcp_resource.icons == icons
+
+ async def test_resource_without_icons(self):
+ """Test that resources work without icons."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.resource("test://resource")
+ def my_resource() -> str:
+ """A resource without an icon."""
+ return "Resource content"
+
+ async with Client(mcp) as client:
+ resources = await client.list_resources()
+ assert len(resources) == 1
+ resource = resources[0]
+ assert resource.icons is None
+
+
+class TestResourceTemplateIcons:
+ """Test icon support for resource templates."""
+
+ async def test_resource_template_with_icons(self):
+ """Test that resource templates can have icons."""
+ mcp = FastMCP("TestServer")
+
+ icons = [Icon(src="https://example.com/template-icon.png")]
+
+ @mcp.resource("test://resource/{id}", icons=icons)
+ def my_template(id: str) -> str:
+ """A resource template with an icon."""
+ return f"Resource {id}"
+
+ async with Client(mcp) as client:
+ templates = await client.list_resource_templates()
+ assert len(templates) == 1
+ template = templates[0]
+ assert template.icons == icons
+
+ async def test_resource_template_from_function_with_icons(self):
+ """Test creating a resource template from a function with icons."""
+ icons = [Icon(src="https://example.com/icon.png")]
+
+ def my_function(id: str) -> str:
+ """A function."""
+ return f"content-{id}"
+
+ template = ResourceTemplate.from_function(
+ my_function,
+ uri_template="test://resource/{id}",
+ icons=icons,
+ )
+ assert template.icons == icons
+
+ # Verify it converts to MCP template correctly
+ mcp_template = template.to_mcp_template()
+ assert mcp_template.icons == icons
+
+ async def test_resource_template_without_icons(self):
+ """Test that resource templates work without icons."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.resource("test://resource/{id}")
+ def my_template(id: str) -> str:
+ """A resource template without an icon."""
+ return f"Resource {id}"
+
+ async with Client(mcp) as client:
+ templates = await client.list_resource_templates()
+ assert len(templates) == 1
+ template = templates[0]
+ assert template.icons is None
+
+
+class TestPromptIcons:
+ """Test icon support for prompts."""
+
+ async def test_prompt_with_icons(self):
+ """Test that prompts can have icons."""
+ mcp = FastMCP("TestServer")
+
+ icons = [Icon(src="https://example.com/prompt-icon.png")]
+
+ @mcp.prompt(icons=icons)
+ def my_prompt(name: str):
+ """A prompt with an icon."""
+ return Message(f"Hello, {name}!")
+
+ async with Client(mcp) as client:
+ prompts = await client.list_prompts()
+ assert len(prompts) == 1
+ prompt = prompts[0]
+ assert prompt.icons == icons
+
+ async def test_prompt_from_function_with_icons(self):
+ """Test creating a prompt from a function with icons."""
+ icons = [Icon(src="https://example.com/icon.png")]
+
+ def my_function(topic: str):
+ """A function."""
+ return Message(f"Tell me about {topic}")
+
+ prompt = Prompt.from_function(my_function, icons=icons)
+ assert prompt.icons == icons
+
+ # Verify it converts to MCP prompt correctly
+ mcp_prompt = prompt.to_mcp_prompt()
+ assert mcp_prompt.icons == icons
+
+ async def test_prompt_without_icons(self):
+ """Test that prompts work without icons."""
+ mcp = FastMCP("TestServer")
+
+ @mcp.prompt
+ def my_prompt(name: str):
+ """A prompt without an icon."""
+ return Message(f"Hello, {name}!")
+
+ async with Client(mcp) as client:
+ prompts = await client.list_prompts()
+ assert len(prompts) == 1
+ prompt = prompts[0]
+ assert prompt.icons is None
+
+
+class TestIconTypes:
+ """Test different types of icon data."""
+
+ async def test_multiple_icon_sizes(self):
+ """Test that multiple icon sizes can be specified."""
+ icons = [
+ Icon(
+ src="https://example.com/icon-48.png",
+ mimeType="image/png",
+ sizes=["48x48"],
+ ),
+ Icon(
+ src="https://example.com/icon-96.png",
+ mimeType="image/png",
+ sizes=["96x96"],
+ ),
+ Icon(
+ src="https://example.com/icon.svg",
+ mimeType="image/svg+xml",
+ sizes=["any"],
+ ),
+ ]
+
+ mcp = FastMCP("TestServer", icons=icons)
+
+ async with Client(mcp) as client:
+ server_info = client.initialize_result.serverInfo
+ assert len(server_info.icons) == 3
+ assert server_info.icons == icons
+
+ async def test_data_uri_icon(self):
+ """Test using data URIs for icons."""
+ # Simple SVG data URI
+ data_uri = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+"
+
+ icons = [Icon(src=data_uri, mimeType="image/svg+xml")]
+
+ mcp = FastMCP("TestServer")
+
+ @mcp.tool(icons=icons)
+ def my_tool() -> str:
+ """A tool with a data URI icon."""
+ return "result"
+
+ async with Client(mcp) as client:
+ tools = await client.list_tools()
+ assert tools[0].icons[0].src == data_uri
+
+ async def test_icon_without_optional_fields(self):
+ """Test that icons work with only the src field."""
+ icons = [Icon(src="https://example.com/icon.png")]
+
+ mcp = FastMCP("TestServer", icons=icons)
+
+ async with Client(mcp) as client:
+ server_info = client.initialize_result.serverInfo
+ assert server_info.icons[0].src == "https://example.com/icon.png"
+ assert server_info.icons[0].mimeType is None
+ assert server_info.icons[0].sizes is None
+
+
+class TestIconImport:
+ """Test that Icon must be imported from mcp.types."""
+
+ def test_icon_import(self):
+ """Test that Icon must be imported from mcp.types, not fastmcp."""
+ # Icon should NOT be available from fastmcp
+ import fastmcp
+
+ assert not hasattr(fastmcp, "Icon")
+
+ # Icon should be imported from mcp.types
+ from mcp.types import Icon as MCPIcon
+
+ icon = MCPIcon(src="https://example.com/icon.png")
+ assert icon.src == "https://example.com/icon.png"
diff --git a/tests/utilities/test_inspect.py b/tests/utilities/test_inspect.py
index 730e297b6..d03305c65 100644
--- a/tests/utilities/test_inspect.py
+++ b/tests/utilities/test_inspect.py
@@ -40,6 +40,8 @@ class TestFastMCPInfo:
mcp_version="1.0.0",
server_generation=2,
version="1.0.0",
+ website_url=None,
+ icons=None,
tools=[tool],
prompts=[],
resources=[],
@@ -66,6 +68,8 @@ class TestFastMCPInfo:
mcp_version="1.0.0",
server_generation=2,
version="1.0.0",
+ website_url=None,
+ icons=None,
tools=[],
prompts=[],
resources=[],
@@ -604,6 +608,330 @@ class TestFastMCP1xCompatibility:
assert len(info2x.templates) == 0
+class TestIconExtraction:
+ """Tests for icon extraction in inspect."""
+
+ async def test_server_icons_and_website(self):
+ """Test that server-level icons and website_url are extracted."""
+ from mcp.types import Icon
+
+ mcp = FastMCP(
+ "IconServer",
+ website_url="https://example.com",
+ icons=[
+ Icon(
+ src="https://example.com/icon.png",
+ mimeType="image/png",
+ sizes=["48x48"],
+ )
+ ],
+ )
+
+ info = await inspect_fastmcp(mcp)
+
+ assert info.website_url == "https://example.com"
+ assert info.icons is not None
+ assert len(info.icons) == 1
+ assert info.icons[0]["src"] == "https://example.com/icon.png"
+ assert info.icons[0]["mimeType"] == "image/png"
+ assert info.icons[0]["sizes"] == ["48x48"]
+
+ async def test_server_without_icons(self):
+ """Test that servers without icons have None for icons and website_url."""
+ mcp = FastMCP("NoIconServer")
+
+ info = await inspect_fastmcp(mcp)
+
+ assert info.website_url is None
+ assert info.icons is None
+
+ async def test_tool_icons(self):
+ """Test that tool icons are extracted."""
+ from mcp.types import Icon
+
+ mcp = FastMCP("ToolIconServer")
+
+ @mcp.tool(
+ icons=[
+ Icon(
+ src="https://example.com/calculator.png",
+ mimeType="image/png",
+ )
+ ]
+ )
+ def calculate(x: int) -> int:
+ """Calculate something."""
+ return x * 2
+
+ @mcp.tool
+ def no_icon_tool() -> str:
+ """Tool without icon."""
+ return "no icon"
+
+ info = await inspect_fastmcp(mcp)
+
+ assert len(info.tools) == 2
+
+ # Find the calculate tool
+ calculate_tool = next(t for t in info.tools if t.name == "calculate")
+ assert calculate_tool.icons is not None
+ assert len(calculate_tool.icons) == 1
+ assert calculate_tool.icons[0]["src"] == "https://example.com/calculator.png"
+
+ # Find the no_icon tool
+ no_icon = next(t for t in info.tools if t.name == "no_icon_tool")
+ assert no_icon.icons is None
+
+ async def test_resource_icons(self):
+ """Test that resource icons are extracted."""
+ from mcp.types import Icon
+
+ mcp = FastMCP("ResourceIconServer")
+
+ @mcp.resource(
+ "resource://data",
+ icons=[Icon(src="https://example.com/data.png", mimeType="image/png")],
+ )
+ def get_data() -> str:
+ """Get data."""
+ return "data"
+
+ @mcp.resource("resource://no-icon")
+ def get_no_icon() -> str:
+ """Get data without icon."""
+ return "no icon"
+
+ info = await inspect_fastmcp(mcp)
+
+ assert len(info.resources) == 2
+
+ # Find the data resource
+ data_resource = next(r for r in info.resources if r.uri == "resource://data")
+ assert data_resource.icons is not None
+ assert len(data_resource.icons) == 1
+ assert data_resource.icons[0]["src"] == "https://example.com/data.png"
+
+ # Find the no-icon resource
+ no_icon = next(r for r in info.resources if r.uri == "resource://no-icon")
+ assert no_icon.icons is None
+
+ async def test_template_icons(self):
+ """Test that resource template icons are extracted."""
+ from mcp.types import Icon
+
+ mcp = FastMCP("TemplateIconServer")
+
+ @mcp.resource(
+ "resource://user/{id}",
+ icons=[Icon(src="https://example.com/user.png", mimeType="image/png")],
+ )
+ def get_user(id: str) -> str:
+ """Get user by ID."""
+ return f"user {id}"
+
+ @mcp.resource("resource://item/{id}")
+ def get_item(id: str) -> str:
+ """Get item without icon."""
+ return f"item {id}"
+
+ info = await inspect_fastmcp(mcp)
+
+ assert len(info.templates) == 2
+
+ # Find the user template
+ user_template = next(
+ t for t in info.templates if t.uri_template == "resource://user/{id}"
+ )
+ assert user_template.icons is not None
+ assert len(user_template.icons) == 1
+ assert user_template.icons[0]["src"] == "https://example.com/user.png"
+
+ # Find the no-icon template
+ no_icon = next(
+ t for t in info.templates if t.uri_template == "resource://item/{id}"
+ )
+ assert no_icon.icons is None
+
+ async def test_prompt_icons(self):
+ """Test that prompt icons are extracted."""
+ from mcp.types import Icon
+
+ mcp = FastMCP("PromptIconServer")
+
+ @mcp.prompt(
+ icons=[Icon(src="https://example.com/analyze.png", mimeType="image/png")]
+ )
+ def analyze(data: str) -> list:
+ """Analyze data."""
+ return [{"role": "user", "content": f"Analyze: {data}"}]
+
+ @mcp.prompt
+ def no_icon_prompt(text: str) -> list:
+ """Prompt without icon."""
+ return [{"role": "user", "content": text}]
+
+ info = await inspect_fastmcp(mcp)
+
+ assert len(info.prompts) == 2
+
+ # Find the analyze prompt
+ analyze_prompt = next(p for p in info.prompts if p.name == "analyze")
+ assert analyze_prompt.icons is not None
+ assert len(analyze_prompt.icons) == 1
+ assert analyze_prompt.icons[0]["src"] == "https://example.com/analyze.png"
+
+ # Find the no-icon prompt
+ no_icon = next(p for p in info.prompts if p.name == "no_icon_prompt")
+ assert no_icon.icons is None
+
+ async def test_multiple_icons(self):
+ """Test that components with multiple icons extract all of them."""
+ from mcp.types import Icon
+
+ mcp = FastMCP(
+ "MultiIconServer",
+ icons=[
+ Icon(
+ src="https://example.com/icon-48.png",
+ mimeType="image/png",
+ sizes=["48x48"],
+ ),
+ Icon(
+ src="https://example.com/icon-96.png",
+ mimeType="image/png",
+ sizes=["96x96"],
+ ),
+ ],
+ )
+
+ @mcp.tool(
+ icons=[
+ Icon(src="https://example.com/tool-small.png", sizes=["24x24"]),
+ Icon(src="https://example.com/tool-large.png", sizes=["48x48"]),
+ ]
+ )
+ def multi_icon_tool() -> str:
+ """Tool with multiple icons."""
+ return "multi"
+
+ info = await inspect_fastmcp(mcp)
+
+ # Check server icons
+ assert info.icons is not None
+ assert len(info.icons) == 2
+ assert info.icons[0]["sizes"] == ["48x48"]
+ assert info.icons[1]["sizes"] == ["96x96"]
+
+ # Check tool icons
+ assert len(info.tools) == 1
+ assert info.tools[0].icons is not None
+ assert len(info.tools[0].icons) == 2
+ assert info.tools[0].icons[0]["sizes"] == ["24x24"]
+ assert info.tools[0].icons[1]["sizes"] == ["48x48"]
+
+ async def test_data_uri_icons(self):
+ """Test that data URI icons are extracted correctly."""
+ from mcp.types import Icon
+
+ data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
+
+ mcp = FastMCP("DataURIServer")
+
+ @mcp.tool(icons=[Icon(src=data_uri, mimeType="image/png")])
+ def data_uri_tool() -> str:
+ """Tool with data URI icon."""
+ return "data"
+
+ info = await inspect_fastmcp(mcp)
+
+ assert len(info.tools) == 1
+ assert info.tools[0].icons is not None
+ assert info.tools[0].icons[0]["src"] == data_uri
+ assert info.tools[0].icons[0]["mimeType"] == "image/png"
+
+ async def test_icons_in_fastmcp_v1(self):
+ """Test that icons are extracted from FastMCP 1.x servers."""
+ from mcp.types import Icon
+
+ mcp = FastMCP1x("Icon1xServer")
+
+ @mcp.tool(
+ icons=[Icon(src="https://example.com/v1-tool.png", mimeType="image/png")]
+ )
+ def v1_tool() -> str:
+ """Tool in v1 server."""
+ return "v1"
+
+ info = await inspect_fastmcp_v1(mcp)
+
+ assert len(info.tools) == 1
+ # v1 servers should also extract icons if present
+ if info.tools[0].icons is not None:
+ assert info.tools[0].icons[0]["src"] == "https://example.com/v1-tool.png"
+
+ async def test_icons_in_formatted_output(self):
+ """Test that icons appear in formatted JSON output."""
+ from mcp.types import Icon
+
+ mcp = FastMCP(
+ "FormattedIconServer",
+ website_url="https://example.com",
+ icons=[Icon(src="https://example.com/server.png", mimeType="image/png")],
+ )
+
+ @mcp.tool(
+ icons=[Icon(src="https://example.com/tool.png", mimeType="image/png")]
+ )
+ def icon_tool() -> str:
+ """Tool with icon."""
+ return "icon"
+
+ info = await inspect_fastmcp(mcp)
+ json_bytes = await format_fastmcp_info(info)
+
+ import json
+
+ data = json.loads(json_bytes)
+
+ # Check server icons in formatted output
+ assert data["server"]["website_url"] == "https://example.com"
+ assert data["server"]["icons"] is not None
+ assert len(data["server"]["icons"]) == 1
+ assert data["server"]["icons"][0]["src"] == "https://example.com/server.png"
+
+ # Check tool icons in formatted output
+ assert len(data["tools"]) == 1
+ assert data["tools"][0]["icons"] is not None
+ assert len(data["tools"][0]["icons"]) == 1
+ assert data["tools"][0]["icons"][0]["src"] == "https://example.com/tool.png"
+
+ async def test_icons_always_present_in_json(self):
+ """Test that icons and website_url fields are always present in JSON, even when None."""
+ mcp = FastMCP("AlwaysPresentServer")
+
+ @mcp.tool
+ def no_icon() -> str:
+ """Tool without icon."""
+ return "none"
+
+ info = await inspect_fastmcp(mcp)
+ json_bytes = await format_fastmcp_info(info)
+
+ import json
+
+ data = json.loads(json_bytes)
+
+ # Fields should always be present, even when None
+ assert "website_url" in data["server"]
+ assert "icons" in data["server"]
+ assert data["server"]["website_url"] is None
+ assert data["server"]["icons"] is None
+
+ assert len(data["tools"]) == 1
+ assert "icons" in data["tools"][0]
+ assert data["tools"][0]["icons"] is None
+
+
class TestFormatFunctions:
"""Tests for the formatting functions."""