Implementation of SEP 973 - Additional metadata + icons support (#1357)
Some checks failed
Main branch checks / checks (push) Has been cancelled

This commit is contained in:
Peter Alexander 2025-09-24 20:29:04 +01:00 • committed by GitHub
commit 71889d7387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 308 additions and 8 deletions

View file

@ -516,6 +516,41 @@ def debug_error(error: str) -> list[base.Message]:
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_ _Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
<!-- /snippet-source --> <!-- /snippet-source -->
### Icons
MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:
```python
from mcp.server.fastmcp import FastMCP, Icon
# Create an icon from a file path or URL
icon = Icon(
src="icon.png",
mimeType="image/png",
sizes="64x64"
)
# Add icons to server
mcp = FastMCP(
"My Server",
website_url="https://example.com",
icons=[icon]
)
# Add icons to tools, resources, and prompts
@mcp.tool(icons=[icon])
def my_tool():
"""Tool with an icon."""
return "result"
@mcp.resource("demo://resource", icons=[icon])
def my_resource():
"""Resource with an icon."""
return "content"
```
_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/fastmcp/icons_demo.py)_
### Images ### Images
FastMCP provides an `Image` class that automatically handles image data: FastMCP provides an `Image` class that automatically handles image data:
@ -898,6 +933,8 @@ The FastMCP server instance accessible via `ctx.fastmcp` provides access to serv
- `ctx.fastmcp.name` - The server's name as defined during initialization - `ctx.fastmcp.name` - The server's name as defined during initialization
- `ctx.fastmcp.instructions` - Server instructions/description provided to clients - `ctx.fastmcp.instructions` - Server instructions/description provided to clients
- `ctx.fastmcp.website_url` - Optional website URL for the server
- `ctx.fastmcp.icons` - Optional list of icons for UI display
- `ctx.fastmcp.settings` - Complete server configuration object containing: - `ctx.fastmcp.settings` - Complete server configuration object containing:
- `debug` - Debug mode flag - `debug` - Debug mode flag
- `log_level` - Current logging level - `log_level` - Current logging level

View file

@ -0,0 +1,55 @@
"""
FastMCP Icons Demo Server
Demonstrates using icons with tools, resources, prompts, and implementation.
"""
import base64
from pathlib import Path
from mcp.server.fastmcp import FastMCP, Icon
# Load the icon file and convert to data URI
icon_path = Path(__file__).parent / "mcp.png"
icon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()
icon_data_uri = f"data:image/png;base64,{icon_data}"
icon_data = Icon(src=icon_data_uri, mimeType="image/png", sizes="64x64")
# Create server with icons in implementation
mcp = FastMCP("Icons Demo Server", website_url="https://github.com/modelcontextprotocol/python-sdk", icons=[icon_data])
@mcp.tool(icons=[icon_data])
def demo_tool(message: str) -> str:
"""A demo tool with an icon."""
return message
@mcp.resource("demo://readme", icons=[icon_data])
def readme_resource() -> str:
"""A demo resource with an icon"""
return "This resource has an icon"
@mcp.prompt("prompt_with_icon", icons=[icon_data])
def prompt_with_icon(text: str) -> str:
"""A demo prompt with an icon"""
return text
@mcp.tool(
icons=[
Icon(src=icon_data_uri, mimeType="image/png", sizes="16x16"),
Icon(src=icon_data_uri, mimeType="image/png", sizes="32x32"),
Icon(src=icon_data_uri, mimeType="image/png", sizes="64x64"),
]
)
def multi_icon_tool(action: str) -> str:
"""A tool demonstrating multiple icons."""
return "multi_icon_tool"
if __name__ == "__main__":
# Run the server
mcp.run()

BIN
examples/fastmcp/mcp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -2,8 +2,10 @@
from importlib.metadata import version from importlib.metadata import version
from mcp.types import Icon
from .server import Context, FastMCP from .server import Context, FastMCP
from .utilities.types import Audio, Image from .utilities.types import Audio, Image
__version__ = version("mcp") __version__ = version("mcp")
__all__ = ["FastMCP", "Context", "Image", "Audio"] __all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]

View file

@ -11,7 +11,7 @@ from pydantic import BaseModel, Field, TypeAdapter, validate_call
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context from mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context
from mcp.server.fastmcp.utilities.func_metadata import func_metadata from mcp.server.fastmcp.utilities.func_metadata import func_metadata
from mcp.types import ContentBlock, TextContent from mcp.types import ContentBlock, Icon, TextContent
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context from mcp.server.fastmcp.server import Context
@ -71,6 +71,7 @@ class Prompt(BaseModel):
description: str | None = Field(None, description="Description of what the prompt does") description: str | None = Field(None, description="Description of what the prompt does")
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt") arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True) fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True) context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
@classmethod @classmethod
@ -80,6 +81,7 @@ class Prompt(BaseModel):
name: str | None = None, name: str | None = None,
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
icons: list[Icon] | None = None,
context_kwarg: str | None = None, context_kwarg: str | None = None,
) -> Prompt: ) -> Prompt:
"""Create a Prompt from a function. """Create a Prompt from a function.
@ -128,6 +130,7 @@ class Prompt(BaseModel):
description=description or fn.__doc__ or "", description=description or fn.__doc__ or "",
arguments=arguments, arguments=arguments,
fn=fn, fn=fn,
icons=icons,
context_kwarg=context_kwarg, context_kwarg=context_kwarg,
) )

View file

@ -13,6 +13,8 @@ from pydantic import (
field_validator, field_validator,
) )
from mcp.types import Icon
class Resource(BaseModel, abc.ABC): class Resource(BaseModel, abc.ABC):
"""Base class for all resources.""" """Base class for all resources."""
@ -28,6 +30,7 @@ class Resource(BaseModel, abc.ABC):
description="MIME type of the resource content", description="MIME type of the resource content",
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$", pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
) )
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource")
@field_validator("name", mode="before") @field_validator("name", mode="before")
@classmethod @classmethod

View file

@ -103,6 +103,7 @@ class ResourceTemplate(BaseModel):
title=self.title, title=self.title,
description=self.description, description=self.description,
mime_type=self.mime_type, mime_type=self.mime_type,
icons=None, # Resource templates don't support icons
fn=lambda: result, # Capture result in closure fn=lambda: result, # Capture result in closure
) )
except Exception as e: except Exception as e:

View file

@ -14,6 +14,7 @@ import pydantic_core
from pydantic import AnyUrl, Field, ValidationInfo, validate_call from pydantic import AnyUrl, Field, ValidationInfo, validate_call
from mcp.server.fastmcp.resources.base import Resource from mcp.server.fastmcp.resources.base import Resource
from mcp.types import Icon
class TextResource(Resource): class TextResource(Resource):
@ -80,6 +81,7 @@ class FunctionResource(Resource):
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
mime_type: str | None = None, mime_type: str | None = None,
icons: list[Icon] | None = None,
) -> "FunctionResource": ) -> "FunctionResource":
"""Create a FunctionResource from a function.""" """Create a FunctionResource from a function."""
func_name = name or fn.__name__ func_name = name or fn.__name__
@ -96,6 +98,7 @@ class FunctionResource(Resource):
description=description or fn.__doc__ or "", description=description or fn.__doc__ or "",
mime_type=mime_type or "text/plain", mime_type=mime_type or "text/plain",
fn=fn, fn=fn,
icons=icons,
) )

View file

@ -43,7 +43,7 @@ from mcp.server.streamable_http import EventStore
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.context import LifespanContextT, RequestContext, RequestT from mcp.shared.context import LifespanContextT, RequestContext, RequestT
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, ToolAnnotations from mcp.types import AnyFunction, ContentBlock, GetPromptResult, Icon, ToolAnnotations
from mcp.types import Prompt as MCPPrompt from mcp.types import Prompt as MCPPrompt
from mcp.types import PromptArgument as MCPPromptArgument from mcp.types import PromptArgument as MCPPromptArgument
from mcp.types import Resource as MCPResource from mcp.types import Resource as MCPResource
@ -120,10 +120,12 @@ def lifespan_wrapper(
class FastMCP(Generic[LifespanResultT]): class FastMCP(Generic[LifespanResultT]):
def __init__( def __init__( # noqa: PLR0913
self, self,
name: str | None = None, name: str | None = None,
instructions: str | None = None, instructions: str | None = None,
website_url: str | None = None,
icons: list[Icon] | None = None,
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None, auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,
token_verifier: TokenVerifier | None = None, token_verifier: TokenVerifier | None = None,
event_store: EventStore | None = None, event_store: EventStore | None = None,
@ -170,6 +172,8 @@ class FastMCP(Generic[LifespanResultT]):
self._mcp_server = MCPServer( self._mcp_server = MCPServer(
name=name or "FastMCP", name=name or "FastMCP",
instructions=instructions, instructions=instructions,
website_url=website_url,
icons=icons,
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server. # TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
# We need to create a Lifespan type that is a generic on the server type, like Starlette does. # We need to create a Lifespan type that is a generic on the server type, like Starlette does.
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
@ -211,6 +215,14 @@ class FastMCP(Generic[LifespanResultT]):
def instructions(self) -> str | None: def instructions(self) -> str | None:
return self._mcp_server.instructions return self._mcp_server.instructions
@property
def website_url(self) -> str | None:
return self._mcp_server.website_url
@property
def icons(self) -> list[Icon] | None:
return self._mcp_server.icons
@property @property
def session_manager(self) -> StreamableHTTPSessionManager: def session_manager(self) -> StreamableHTTPSessionManager:
"""Get the StreamableHTTP session manager. """Get the StreamableHTTP session manager.
@ -277,6 +289,7 @@ class FastMCP(Generic[LifespanResultT]):
inputSchema=info.parameters, inputSchema=info.parameters,
outputSchema=info.output_schema, outputSchema=info.output_schema,
annotations=info.annotations, annotations=info.annotations,
icons=info.icons,
) )
for info in tools for info in tools
] ]
@ -308,6 +321,7 @@ class FastMCP(Generic[LifespanResultT]):
title=resource.title, title=resource.title,
description=resource.description, description=resource.description,
mimeType=resource.mime_type, mimeType=resource.mime_type,
icons=resource.icons,
) )
for resource in resources for resource in resources
] ]
@ -347,6 +361,7 @@ class FastMCP(Generic[LifespanResultT]):
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
annotations: ToolAnnotations | None = None, annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None, structured_output: bool | None = None,
) -> None: ) -> None:
"""Add a tool to the server. """Add a tool to the server.
@ -371,6 +386,7 @@ class FastMCP(Generic[LifespanResultT]):
title=title, title=title,
description=description, description=description,
annotations=annotations, annotations=annotations,
icons=icons,
structured_output=structured_output, structured_output=structured_output,
) )
@ -380,6 +396,7 @@ class FastMCP(Generic[LifespanResultT]):
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
annotations: ToolAnnotations | None = None, annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None, structured_output: bool | None = None,
) -> Callable[[AnyFunction], AnyFunction]: ) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a tool. """Decorator to register a tool.
@ -426,6 +443,7 @@ class FastMCP(Generic[LifespanResultT]):
title=title, title=title,
description=description, description=description,
annotations=annotations, annotations=annotations,
icons=icons,
structured_output=structured_output, structured_output=structured_output,
) )
return fn return fn
@ -466,6 +484,7 @@ class FastMCP(Generic[LifespanResultT]):
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
mime_type: str | None = None, mime_type: str | None = None,
icons: list[Icon] | None = None,
) -> Callable[[AnyFunction], AnyFunction]: ) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a function as a resource. """Decorator to register a function as a resource.
@ -540,6 +559,7 @@ class FastMCP(Generic[LifespanResultT]):
title=title, title=title,
description=description, description=description,
mime_type=mime_type, mime_type=mime_type,
# Note: Resource templates don't support icons
) )
else: else:
# Register as regular resource # Register as regular resource
@ -550,6 +570,7 @@ class FastMCP(Generic[LifespanResultT]):
title=title, title=title,
description=description, description=description,
mime_type=mime_type, mime_type=mime_type,
icons=icons,
) )
self.add_resource(resource) self.add_resource(resource)
return fn return fn
@ -565,7 +586,11 @@ class FastMCP(Generic[LifespanResultT]):
self._prompt_manager.add_prompt(prompt) self._prompt_manager.add_prompt(prompt)
def prompt( def prompt(
self, name: str | None = None, title: str | None = None, description: str | None = None self,
name: str | None = None,
title: str | None = None,
description: str | None = None,
icons: list[Icon] | None = None,
) -> Callable[[AnyFunction], AnyFunction]: ) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a prompt. """Decorator to register a prompt.
@ -609,7 +634,7 @@ class FastMCP(Generic[LifespanResultT]):
) )
def decorator(func: AnyFunction) -> AnyFunction: def decorator(func: AnyFunction) -> AnyFunction:
prompt = Prompt.from_function(func, name=name, title=title, description=description) prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
self.add_prompt(prompt) self.add_prompt(prompt)
return func return func
@ -980,6 +1005,7 @@ class FastMCP(Generic[LifespanResultT]):
) )
for arg in (prompt.arguments or []) for arg in (prompt.arguments or [])
], ],
icons=prompt.icons,
) )
for prompt in prompts for prompt in prompts
] ]

View file

@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
from mcp.server.fastmcp.exceptions import ToolError from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter from mcp.server.fastmcp.utilities.context_injection import find_context_parameter
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
from mcp.types import ToolAnnotations from mcp.types import Icon, ToolAnnotations
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context from mcp.server.fastmcp.server import Context
@ -33,6 +33,7 @@ class Tool(BaseModel):
is_async: bool = Field(description="Whether the tool is async") is_async: bool = Field(description="Whether the tool is async")
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context") context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
annotations: ToolAnnotations | None = Field(None, description="Optional annotations for the tool") annotations: ToolAnnotations | None = Field(None, description="Optional annotations for the tool")
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this tool")
@cached_property @cached_property
def output_schema(self) -> dict[str, Any] | None: def output_schema(self) -> dict[str, Any] | None:
@ -47,6 +48,7 @@ class Tool(BaseModel):
description: str | None = None, description: str | None = None,
context_kwarg: str | None = None, context_kwarg: str | None = None,
annotations: ToolAnnotations | None = None, annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None, structured_output: bool | None = None,
) -> Tool: ) -> Tool:
"""Create a Tool from a function.""" """Create a Tool from a function."""
@ -78,6 +80,7 @@ class Tool(BaseModel):
is_async=is_async, is_async=is_async,
context_kwarg=context_kwarg, context_kwarg=context_kwarg,
annotations=annotations, annotations=annotations,
icons=icons,
) )
async def run( async def run(

View file

@ -7,7 +7,7 @@ from mcp.server.fastmcp.exceptions import ToolError
from mcp.server.fastmcp.tools.base import Tool from mcp.server.fastmcp.tools.base import Tool
from mcp.server.fastmcp.utilities.logging import get_logger from mcp.server.fastmcp.utilities.logging import get_logger
from mcp.shared.context import LifespanContextT, RequestT from mcp.shared.context import LifespanContextT, RequestT
from mcp.types import ToolAnnotations from mcp.types import Icon, ToolAnnotations
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context from mcp.server.fastmcp.server import Context
@ -49,6 +49,7 @@ class ToolManager:
title: str | None = None, title: str | None = None,
description: str | None = None, description: str | None = None,
annotations: ToolAnnotations | None = None, annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None, structured_output: bool | None = None,
) -> Tool: ) -> Tool:
"""Add a tool to the server.""" """Add a tool to the server."""
@ -58,6 +59,7 @@ class ToolManager:
title=title, title=title,
description=description, description=description,
annotations=annotations, annotations=annotations,
icons=icons,
structured_output=structured_output, structured_output=structured_output,
) )
existing = self._tools.get(tool.name) existing = self._tools.get(tool.name)

View file

@ -136,6 +136,8 @@ class Server(Generic[LifespanResultT, RequestT]):
name: str, name: str,
version: str | None = None, version: str | None = None,
instructions: str | None = None, instructions: str | None = None,
website_url: str | None = None,
icons: list[types.Icon] | None = None,
lifespan: Callable[ lifespan: Callable[
[Server[LifespanResultT, RequestT]], [Server[LifespanResultT, RequestT]],
AbstractAsyncContextManager[LifespanResultT], AbstractAsyncContextManager[LifespanResultT],
@ -144,6 +146,8 @@ class Server(Generic[LifespanResultT, RequestT]):
self.name = name self.name = name
self.version = version self.version = version
self.instructions = instructions self.instructions = instructions
self.website_url = website_url
self.icons = icons
self.lifespan = lifespan self.lifespan = lifespan
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = { self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
types.PingRequest: _ping_handler, types.PingRequest: _ping_handler,
@ -177,6 +181,8 @@ class Server(Generic[LifespanResultT, RequestT]):
experimental_capabilities or {}, experimental_capabilities or {},
), ),
instructions=self.instructions, instructions=self.instructions,
website_url=self.website_url,
icons=self.icons,
) )
def get_capabilities( def get_capabilities(

View file

@ -6,6 +6,7 @@ and tools.
from pydantic import BaseModel from pydantic import BaseModel
from mcp.types import ( from mcp.types import (
Icon,
ServerCapabilities, ServerCapabilities,
) )
@ -15,3 +16,5 @@ class InitializationOptions(BaseModel):
server_version: str server_version: str
capabilities: ServerCapabilities capabilities: ServerCapabilities
instructions: str | None = None instructions: str | None = None
website_url: str | None = None
icons: list[Icon] | None = None

View file

@ -156,6 +156,8 @@ class ServerSession(
serverInfo=types.Implementation( serverInfo=types.Implementation(
name=self._init_options.server_name, name=self._init_options.server_name,
version=self._init_options.server_version, version=self._init_options.server_version,
websiteUrl=self._init_options.website_url,
icons=self._init_options.icons,
), ),
instructions=self._init_options.instructions, instructions=self._init_options.instructions,
) )

View file

@ -213,10 +213,32 @@ class BaseMetadata(BaseModel):
""" """
class Icon(BaseModel):
"""An icon for display in user interfaces."""
src: str
"""URL or data URI for the icon."""
mimeType: str | None = None
"""Optional MIME type for the icon."""
sizes: str | None = None
"""Optional string specifying icon dimensions (e.g., "48x48 96x96")."""
model_config = ConfigDict(extra="allow")
class Implementation(BaseMetadata): class Implementation(BaseMetadata):
"""Describes the name and version of an MCP implementation.""" """Describes the name and version of an MCP implementation."""
version: str version: str
websiteUrl: str | None = None
"""An optional URL of the website for this implementation."""
icons: list[Icon] | None = None
"""An optional list of icons for this implementation."""
model_config = ConfigDict(extra="allow") model_config = ConfigDict(extra="allow")
@ -422,6 +444,8 @@ class Resource(BaseMetadata):
This can be used by Hosts to display file sizes and estimate context window usage. This can be used by Hosts to display file sizes and estimate context window usage.
""" """
icons: list[Icon] | None = None
"""An optional list of icons for this resource."""
annotations: Annotations | None = None annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: dict[str, Any] | None = Field(alias="_meta", default=None)
""" """
@ -628,6 +652,8 @@ class Prompt(BaseMetadata):
"""An optional description of what this prompt provides.""" """An optional description of what this prompt provides."""
arguments: list[PromptArgument] | None = None arguments: list[PromptArgument] | None = None
"""A list of arguments to use for templating the prompt.""" """A list of arguments to use for templating the prompt."""
icons: list[Icon] | None = None
"""An optional list of icons for this prompt."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: dict[str, Any] | None = Field(alias="_meta", default=None)
""" """
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields) See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
@ -852,6 +878,8 @@ class Tool(BaseMetadata):
An optional JSON Schema object defining the structure of the tool's output An optional JSON Schema object defining the structure of the tool's output
returned in the structuredContent field of a CallToolResult. returned in the structuredContent field of a CallToolResult.
""" """
icons: list[Icon] | None = None
"""An optional list of icons for this tool."""
annotations: ToolAnnotations | None = None annotations: ToolAnnotations | None = None
"""Optional additional tool information.""" """Optional additional tool information."""
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: dict[str, Any] | None = Field(alias="_meta", default=None)

View file

@ -0,0 +1,126 @@
"""Test icon and metadata support (SEP-973)."""
import pytest
from mcp.server.fastmcp import FastMCP
from mcp.types import Icon
pytestmark = pytest.mark.anyio
async def test_icons_and_website_url():
"""Test that icons and websiteUrl are properly returned in API calls."""
# Create test icon
test_icon = Icon(
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
mimeType="image/png",
sizes="1x1",
)
# Create server with website URL and icon
mcp = FastMCP("TestServer", website_url="https://example.com", icons=[test_icon])
# Create tool with icon
@mcp.tool(icons=[test_icon])
def test_tool(message: str) -> str:
"""A test tool with an icon."""
return message
# Create resource with icon
@mcp.resource("test://resource", icons=[test_icon])
def test_resource() -> str:
"""A test resource with an icon."""
return "test content"
# Create prompt with icon
@mcp.prompt("test_prompt", icons=[test_icon])
def test_prompt(text: str) -> str:
"""A test prompt with an icon."""
return text
# Test server metadata includes websiteUrl and icons
assert mcp.name == "TestServer"
assert mcp.website_url == "https://example.com"
assert mcp.icons is not None
assert len(mcp.icons) == 1
assert mcp.icons[0].src == test_icon.src
assert mcp.icons[0].mimeType == test_icon.mimeType
assert mcp.icons[0].sizes == test_icon.sizes
# Test tool includes icon
tools = await mcp.list_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.name == "test_tool"
assert tool.icons is not None
assert len(tool.icons) == 1
assert tool.icons[0].src == test_icon.src
# Test resource includes icon
resources = await mcp.list_resources()
assert len(resources) == 1
resource = resources[0]
assert str(resource.uri) == "test://resource"
assert resource.icons is not None
assert len(resource.icons) == 1
assert resource.icons[0].src == test_icon.src
# Test prompt includes icon
prompts = await mcp.list_prompts()
assert len(prompts) == 1
prompt = prompts[0]
assert prompt.name == "test_prompt"
assert prompt.icons is not None
assert len(prompt.icons) == 1
assert prompt.icons[0].src == test_icon.src
async def test_multiple_icons():
"""Test that multiple icons can be added to tools, resources, and prompts."""
# Create multiple test icons
icon1 = Icon(src="data:image/png;base64,icon1", mimeType="image/png", sizes="16x16")
icon2 = Icon(src="data:image/png;base64,icon2", mimeType="image/png", sizes="32x32")
icon3 = Icon(src="data:image/png;base64,icon3", mimeType="image/png", sizes="64x64")
mcp = FastMCP("MultiIconServer")
# Create tool with multiple icons
@mcp.tool(icons=[icon1, icon2, icon3])
def multi_icon_tool() -> str:
"""A tool with multiple icons."""
return "success"
# Test tool has all icons
tools = await mcp.list_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.icons is not None
assert len(tool.icons) == 3
assert tool.icons[0].sizes == "16x16"
assert tool.icons[1].sizes == "32x32"
assert tool.icons[2].sizes == "64x64"
async def test_no_icons_or_website():
"""Test that server works without icons or websiteUrl."""
mcp = FastMCP("BasicServer")
@mcp.tool()
def basic_tool() -> str:
"""A basic tool without icons."""
return "success"
# Test server metadata has no websiteUrl or icons
assert mcp.name == "BasicServer"
assert mcp.website_url is None
assert mcp.icons is None
# Test tool has no icons
tools = await mcp.list_tools()
assert len(tools) == 1
tool = tools[0]
assert tool.name == "basic_tool"
assert tool.icons is None