Implement icon support (#2121)

* Implement icon support in fastmcp

* Fix icon feature tests

- Update snapshot for ResourceTemplate to include icons field
- Remove OAuth mounting tests (belong to PR #2119, not this feature)

* Update docs
This commit is contained in:
Jeremiah Lowin 2025-10-17 17:28:08 -04:00 committed by GitHub
commit 6f627b58fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 944 additions and 8 deletions

View file

@ -104,6 +104,7 @@
"group": "Advanced Features",
"icon": "stars",
"pages": [
"servers/icons",
"servers/context",
"servers/proxy",
"servers/composition",

128
docs/servers/icons.mdx Normal file
View file

@ -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'
<VersionBadge version="2.14.0" />
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"
```

View file

@ -97,9 +97,15 @@ def data_analysis_prompt(
A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information
</ParamField>
<ParamField body="icons" type="list[Icon] | None">
<VersionBadge version="2.14.0" />
Optional list of icon representations for this prompt. See [Icons](/servers/icons) for detailed examples
</ParamField>
<ParamField body="meta" type="dict[str, Any] | None">
<VersionBadge version="2.11.0" />
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.
</ParamField>
</Card>

View file

@ -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
</ParamField>
<ParamField body="icons" type="list[Icon] | None">
<VersionBadge version="2.14.0" />
Optional list of icon representations for this resource or template. See [Icons](/servers/icons) for detailed examples
</ParamField>
<ParamField body="annotations" type="Annotations | dict | None">
An optional `Annotations` object or dictionary to add additional metadata about the resource.
<Expandable title="Annotations attributes">

View file

@ -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
</ParamField>
<ParamField body="version" type="str | None">
Version string for your server. If not provided, defaults to the FastMCP library version
</ParamField>
<ParamField body="website_url" type="str | None">
<VersionBadge version="2.14.0" />
URL to a website with more information about your server. Displayed in client applications
</ParamField>
<ParamField body="icons" type="list[Icon] | None">
<VersionBadge version="2.14.0" />
List of icon representations for your server. Icons help users visually identify your server in client applications. See [Icons](/servers/icons) for detailed examples
</ParamField>
<ParamField body="auth" type="OAuthProvider | TokenVerifier | None">
Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration options
</ParamField>

View file

@ -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
</ParamField>
<ParamField body="icons" type="list[Icon] | None">
<VersionBadge version="2.14.0" />
Optional list of icon representations for this tool. See [Icons](/servers/icons) for detailed examples
</ParamField>
<ParamField body="exclude_args" type="list[str] | None">
A list of argument names to exclude from the tool schema shown to the LLM. See [Excluding Arguments](#excluding-arguments) for more information
</ParamField>

View file

@ -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}")

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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.",

View file

@ -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,
},

View file

@ -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}',
]
)

342
tests/server/test_icons.py Normal file
View file

@ -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"

View file

@ -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."""