Skip to content
Merged
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,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)_
<!-- /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

FastMCP provides an `Image` class that automatically handles image data:
Expand Down Expand Up @@ -895,6 +930,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.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:
- `debug` - Debug mode flag
- `log_level` - Current logging level
Expand Down
55 changes: 55 additions & 0 deletions examples/fastmcp/icons_demo.py
Original file line number Diff line number Diff line change
@@ -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()
Binary file added examples/fastmcp/mcp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/mcp/server/fastmcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from importlib.metadata import version

from mcp.types import Icon

from .server import Context, FastMCP
from .utilities.types import Audio, Image

__version__ = version("mcp")
__all__ = ["FastMCP", "Context", "Image", "Audio"]
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]
5 changes: 4 additions & 1 deletion src/mcp/server/fastmcp/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pydantic_core
from pydantic import BaseModel, Field, TypeAdapter, validate_call

from mcp.types import ContentBlock, TextContent
from mcp.types import ContentBlock, Icon, TextContent


class Message(BaseModel):
Expand Down Expand Up @@ -62,6 +62,7 @@ class Prompt(BaseModel):
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")
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")

@classmethod
def from_function(
Expand All @@ -70,6 +71,7 @@ def from_function(
name: str | None = None,
title: str | None = None,
description: str | None = None,
icons: list[Icon] | None = None,
) -> "Prompt":
"""Create a Prompt from a function.

Expand Down Expand Up @@ -109,6 +111,7 @@ def from_function(
description=description or fn.__doc__ or "",
arguments=arguments,
fn=fn,
icons=icons,
)

async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
Expand Down
3 changes: 3 additions & 0 deletions src/mcp/server/fastmcp/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
field_validator,
)

from mcp.types import Icon


class Resource(BaseModel, abc.ABC):
"""Base class for all resources."""
Expand All @@ -28,6 +30,7 @@ class Resource(BaseModel, abc.ABC):
description="MIME type of the resource content",
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")
@classmethod
Expand Down
1 change: 1 addition & 0 deletions src/mcp/server/fastmcp/resources/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
title=self.title,
description=self.description,
mime_type=self.mime_type,
icons=None, # Resource templates don't support icons
fn=lambda: result, # Capture result in closure
)
except Exception as e:
Expand Down
3 changes: 3 additions & 0 deletions src/mcp/server/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pydantic import AnyUrl, Field, ValidationInfo, validate_call

from mcp.server.fastmcp.resources.base import Resource
from mcp.types import Icon


class TextResource(Resource):
Expand Down Expand Up @@ -80,6 +81,7 @@ def from_function(
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
icons: list[Icon] | None = None,
) -> "FunctionResource":
"""Create a FunctionResource from a function."""
func_name = name or fn.__name__
Expand All @@ -96,6 +98,7 @@ def from_function(
description=description or fn.__doc__ or "",
mime_type=mime_type or "text/plain",
fn=fn,
icons=icons,
)


Expand Down
34 changes: 30 additions & 4 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings
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 PromptArgument as MCPPromptArgument
from mcp.types import Resource as MCPResource
Expand Down Expand Up @@ -119,10 +119,12 @@ async def wrap(_: MCPServer[LifespanResultT, Request]) -> AsyncIterator[Lifespan


class FastMCP(Generic[LifespanResultT]):
def __init__(
def __init__( # noqa: PLR0913
self,
name: 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,
token_verifier: TokenVerifier | None = None,
event_store: EventStore | None = None,
Expand Down Expand Up @@ -169,6 +171,8 @@ def __init__(
self._mcp_server = MCPServer(
name=name or "FastMCP",
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.
# 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
Expand Down Expand Up @@ -210,6 +214,14 @@ def name(self) -> str:
def instructions(self) -> str | None:
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
def session_manager(self) -> StreamableHTTPSessionManager:
"""Get the StreamableHTTP session manager.
Expand Down Expand Up @@ -276,6 +288,7 @@ async def list_tools(self) -> list[MCPTool]:
inputSchema=info.parameters,
outputSchema=info.output_schema,
annotations=info.annotations,
icons=info.icons,
)
for info in tools
]
Expand Down Expand Up @@ -307,6 +320,7 @@ async def list_resources(self) -> list[MCPResource]:
title=resource.title,
description=resource.description,
mimeType=resource.mime_type,
icons=resource.icons,
)
for resource in resources
]
Expand Down Expand Up @@ -344,6 +358,7 @@ def add_tool(
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None,
) -> None:
"""Add a tool to the server.
Expand All @@ -368,6 +383,7 @@ def add_tool(
title=title,
description=description,
annotations=annotations,
icons=icons,
structured_output=structured_output,
)

Expand All @@ -377,6 +393,7 @@ def tool(
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a tool.
Expand Down Expand Up @@ -423,6 +440,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
title=title,
description=description,
annotations=annotations,
icons=icons,
structured_output=structured_output,
)
return fn
Expand Down Expand Up @@ -463,6 +481,7 @@ def resource(
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
icons: list[Icon] | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a function as a resource.

Expand Down Expand Up @@ -531,6 +550,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
title=title,
description=description,
mime_type=mime_type,
# Note: Resource templates don't support icons
)
else:
# Register as regular resource
Expand All @@ -541,6 +561,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
title=title,
description=description,
mime_type=mime_type,
icons=icons,
)
self.add_resource(resource)
return fn
Expand All @@ -556,7 +577,11 @@ def add_prompt(self, prompt: Prompt) -> None:
self._prompt_manager.add_prompt(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]:
"""Decorator to register a prompt.

Expand Down Expand Up @@ -600,7 +625,7 @@ async def analyze_file(path: str) -> list[Message]:
)

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)
return func

Expand Down Expand Up @@ -971,6 +996,7 @@ async def list_prompts(self) -> list[MCPPrompt]:
)
for arg in (prompt.arguments or [])
],
icons=prompt.icons,
)
for prompt in prompts
]
Expand Down
5 changes: 4 additions & 1 deletion src/mcp/server/fastmcp/tools/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from mcp.server.fastmcp.exceptions import ToolError
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:
from mcp.server.fastmcp.server import Context
Expand All @@ -32,6 +32,7 @@ class Tool(BaseModel):
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")
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
def output_schema(self) -> dict[str, Any] | None:
Expand All @@ -46,6 +47,7 @@ def from_function(
description: str | None = None,
context_kwarg: str | None = None,
annotations: ToolAnnotations | None = None,
icons: list[Icon] | None = None,
structured_output: bool | None = None,
) -> Tool:
"""Create a Tool from a function."""
Expand Down Expand Up @@ -85,6 +87,7 @@ def from_function(
is_async=is_async,
context_kwarg=context_kwarg,
annotations=annotations,
icons=icons,
)

async def run(
Expand Down
Loading
Loading