Skip to content

Commit 6ad4bb3

Browse files
committed
Implementation of SEP-973
1 parent c47c767 commit 6ad4bb3

File tree

13 files changed

+137
-6
lines changed

13 files changed

+137
-6
lines changed

examples/fastmcp/icons_demo.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
FastMCP Icons Demo Server
3+
4+
Demonstrates using icons with tools, resources, prompts, and implementation.
5+
"""
6+
7+
import base64
8+
from pathlib import Path
9+
10+
from mcp.server.fastmcp import FastMCP
11+
from mcp.types import Icon
12+
13+
# Load the icon file and convert to data URI
14+
icon_path = Path(__file__).parent / "mcp.png"
15+
with open(icon_path, "rb") as f:
16+
icon_data = base64.b64encode(f.read()).decode("utf-8")
17+
icon_data_uri = f"data:image/png;base64,{icon_data}"
18+
19+
icon_data = Icon(src=icon_data_uri, mimeType="image/png", sizes="32x32")
20+
21+
# Create server with icons in implementation
22+
mcp = FastMCP(
23+
"Icons Demo Server", website_url="https://https://github.com/modelcontextprotocol/python-sdk", icons=[icon_data]
24+
)
25+
26+
27+
@mcp.tool(icons=[icon_data])
28+
def demo_tool(message: str) -> str:
29+
"""A demo tool with an icon."""
30+
return message
31+
32+
33+
@mcp.resource("demo://readme", icons=[icon_data])
34+
def readme_resource() -> str:
35+
"""A demo resource with an icon"""
36+
return "This resource has an icon"
37+
38+
39+
@mcp.prompt("prompt_with_icon", icons=[icon_data])
40+
def prompt_with_icon(text: str) -> str:
41+
"""A demo prompt with an icon"""
42+
return text
43+
44+
45+
@mcp.tool(
46+
icons=[
47+
Icon(src=icon_data_uri, mimeType="image/png", sizes="16x16"),
48+
Icon(src=icon_data_uri, mimeType="image/png", sizes="32x32"),
49+
Icon(src=icon_data_uri, mimeType="image/png", sizes="64x64"),
50+
]
51+
)
52+
def multi_icon_tool(action: str) -> str:
53+
"""A tool demonstrating multiple icons."""
54+
return "multi_icon_tool"
55+
56+
57+
if __name__ == "__main__":
58+
# Run the server
59+
mcp.run()

examples/fastmcp/mcp.png

61.4 KB
Loading

src/mcp/server/fastmcp/prompts/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pydantic_core
88
from pydantic import BaseModel, Field, TypeAdapter, validate_call
99

10-
from mcp.types import ContentBlock, TextContent
10+
from mcp.types import ContentBlock, Icon, TextContent
1111

1212

1313
class Message(BaseModel):
@@ -62,6 +62,7 @@ class Prompt(BaseModel):
6262
description: str | None = Field(None, description="Description of what the prompt does")
6363
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
6464
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
65+
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
6566

6667
@classmethod
6768
def from_function(
@@ -70,6 +71,7 @@ def from_function(
7071
name: str | None = None,
7172
title: str | None = None,
7273
description: str | None = None,
74+
icons: list[Icon] | None = None,
7375
) -> "Prompt":
7476
"""Create a Prompt from a function.
7577
@@ -109,6 +111,7 @@ def from_function(
109111
description=description or fn.__doc__ or "",
110112
arguments=arguments,
111113
fn=fn,
114+
icons=icons,
112115
)
113116

114117
async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:

src/mcp/server/fastmcp/resources/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
field_validator,
1414
)
1515

16+
from mcp.types import Icon
17+
1618

1719
class Resource(BaseModel, abc.ABC):
1820
"""Base class for all resources."""
@@ -28,6 +30,7 @@ class Resource(BaseModel, abc.ABC):
2830
description="MIME type of the resource content",
2931
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
3032
)
33+
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource")
3134

3235
@field_validator("name", mode="before")
3336
@classmethod

src/mcp/server/fastmcp/resources/templates.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
7878
description=self.description,
7979
mime_type=self.mime_type,
8080
fn=lambda: result, # Capture result in closure
81+
icons=None, # Resource templates don't support icons
8182
)
8283
except Exception as e:
8384
raise ValueError(f"Error creating resource from template: {e}")

src/mcp/server/fastmcp/resources/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
1515

1616
from mcp.server.fastmcp.resources.base import Resource
17+
from mcp.types import Icon
1718

1819

1920
class TextResource(Resource):
@@ -80,6 +81,7 @@ def from_function(
8081
title: str | None = None,
8182
description: str | None = None,
8283
mime_type: str | None = None,
84+
icons: list[Icon] | None = None,
8385
) -> "FunctionResource":
8486
"""Create a FunctionResource from a function."""
8587
func_name = name or fn.__name__
@@ -96,6 +98,7 @@ def from_function(
9698
description=description or fn.__doc__ or "",
9799
mime_type=mime_type or "text/plain",
98100
fn=fn,
101+
icons=icons,
99102
)
100103

101104

src/mcp/server/fastmcp/server.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
4343
from mcp.server.transport_security import TransportSecuritySettings
4444
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
45-
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, ToolAnnotations
45+
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, Icon, ToolAnnotations
4646
from mcp.types import Prompt as MCPPrompt
4747
from mcp.types import PromptArgument as MCPPromptArgument
4848
from mcp.types import Resource as MCPResource
@@ -123,6 +123,8 @@ def __init__(
123123
self,
124124
name: str | None = None,
125125
instructions: str | None = None,
126+
website_url: str | None = None,
127+
icons: list[Icon] | None = None,
126128
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,
127129
token_verifier: TokenVerifier | None = None,
128130
event_store: EventStore | None = None,
@@ -169,6 +171,8 @@ def __init__(
169171
self._mcp_server = MCPServer(
170172
name=name or "FastMCP",
171173
instructions=instructions,
174+
website_url=website_url,
175+
icons=icons,
172176
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
173177
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
174178
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
@@ -276,6 +280,7 @@ async def list_tools(self) -> list[MCPTool]:
276280
inputSchema=info.parameters,
277281
outputSchema=info.output_schema,
278282
annotations=info.annotations,
283+
icons=info.icons,
279284
)
280285
for info in tools
281286
]
@@ -307,6 +312,7 @@ async def list_resources(self) -> list[MCPResource]:
307312
title=resource.title,
308313
description=resource.description,
309314
mimeType=resource.mime_type,
315+
icons=resource.icons,
310316
)
311317
for resource in resources
312318
]
@@ -344,6 +350,7 @@ def add_tool(
344350
title: str | None = None,
345351
description: str | None = None,
346352
annotations: ToolAnnotations | None = None,
353+
icons: list[Icon] | None = None,
347354
structured_output: bool | None = None,
348355
) -> None:
349356
"""Add a tool to the server.
@@ -368,6 +375,7 @@ def add_tool(
368375
title=title,
369376
description=description,
370377
annotations=annotations,
378+
icons=icons,
371379
structured_output=structured_output,
372380
)
373381

@@ -377,6 +385,7 @@ def tool(
377385
title: str | None = None,
378386
description: str | None = None,
379387
annotations: ToolAnnotations | None = None,
388+
icons: list[Icon] | None = None,
380389
structured_output: bool | None = None,
381390
) -> Callable[[AnyFunction], AnyFunction]:
382391
"""Decorator to register a tool.
@@ -423,6 +432,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
423432
title=title,
424433
description=description,
425434
annotations=annotations,
435+
icons=icons,
426436
structured_output=structured_output,
427437
)
428438
return fn
@@ -463,6 +473,7 @@ def resource(
463473
title: str | None = None,
464474
description: str | None = None,
465475
mime_type: str | None = None,
476+
icons: list[Icon] | None = None,
466477
) -> Callable[[AnyFunction], AnyFunction]:
467478
"""Decorator to register a function as a resource.
468479
@@ -531,6 +542,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
531542
title=title,
532543
description=description,
533544
mime_type=mime_type,
545+
# Note: Resource templates don't support icons
534546
)
535547
else:
536548
# Register as regular resource
@@ -541,6 +553,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
541553
title=title,
542554
description=description,
543555
mime_type=mime_type,
556+
icons=icons,
544557
)
545558
self.add_resource(resource)
546559
return fn
@@ -556,7 +569,11 @@ def add_prompt(self, prompt: Prompt) -> None:
556569
self._prompt_manager.add_prompt(prompt)
557570

558571
def prompt(
559-
self, name: str | None = None, title: str | None = None, description: str | None = None
572+
self,
573+
name: str | None = None,
574+
title: str | None = None,
575+
description: str | None = None,
576+
icons: list[Icon] | None = None,
560577
) -> Callable[[AnyFunction], AnyFunction]:
561578
"""Decorator to register a prompt.
562579
@@ -600,7 +617,7 @@ async def analyze_file(path: str) -> list[Message]:
600617
)
601618

602619
def decorator(func: AnyFunction) -> AnyFunction:
603-
prompt = Prompt.from_function(func, name=name, title=title, description=description)
620+
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
604621
self.add_prompt(prompt)
605622
return func
606623

@@ -971,6 +988,7 @@ async def list_prompts(self) -> list[MCPPrompt]:
971988
)
972989
for arg in (prompt.arguments or [])
973990
],
991+
icons=prompt.icons,
974992
)
975993
for prompt in prompts
976994
]

src/mcp/server/fastmcp/tools/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from mcp.server.fastmcp.exceptions import ToolError
1212
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
13-
from mcp.types import ToolAnnotations
13+
from mcp.types import Icon, ToolAnnotations
1414

1515
if TYPE_CHECKING:
1616
from mcp.server.fastmcp.server import Context
@@ -32,6 +32,7 @@ class Tool(BaseModel):
3232
is_async: bool = Field(description="Whether the tool is async")
3333
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
3434
annotations: ToolAnnotations | None = Field(None, description="Optional annotations for the tool")
35+
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this tool")
3536

3637
@cached_property
3738
def output_schema(self) -> dict[str, Any] | None:
@@ -46,6 +47,7 @@ def from_function(
4647
description: str | None = None,
4748
context_kwarg: str | None = None,
4849
annotations: ToolAnnotations | None = None,
50+
icons: list[Icon] | None = None,
4951
structured_output: bool | None = None,
5052
) -> Tool:
5153
"""Create a Tool from a function."""
@@ -85,6 +87,7 @@ def from_function(
8587
is_async=is_async,
8688
context_kwarg=context_kwarg,
8789
annotations=annotations,
90+
icons=icons,
8891
)
8992

9093
async def run(

src/mcp/server/fastmcp/tools/tool_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mcp.server.fastmcp.tools.base import Tool
88
from mcp.server.fastmcp.utilities.logging import get_logger
99
from mcp.shared.context import LifespanContextT, RequestT
10-
from mcp.types import ToolAnnotations
10+
from mcp.types import Icon, ToolAnnotations
1111

1212
if TYPE_CHECKING:
1313
from mcp.server.fastmcp.server import Context
@@ -49,6 +49,7 @@ def add_tool(
4949
title: str | None = None,
5050
description: str | None = None,
5151
annotations: ToolAnnotations | None = None,
52+
icons: list[Icon] | None = None,
5253
structured_output: bool | None = None,
5354
) -> Tool:
5455
"""Add a tool to the server."""
@@ -58,6 +59,7 @@ def add_tool(
5859
title=title,
5960
description=description,
6061
annotations=annotations,
62+
icons=icons,
6163
structured_output=structured_output,
6264
)
6365
existing = self._tools.get(tool.name)

src/mcp/server/lowlevel/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ def __init__(
135135
name: str,
136136
version: str | None = None,
137137
instructions: str | None = None,
138+
website_url: str | None = None,
139+
icons: list[types.Icon] | None = None,
138140
lifespan: Callable[
139141
[Server[LifespanResultT, RequestT]],
140142
AbstractAsyncContextManager[LifespanResultT],
@@ -143,6 +145,8 @@ def __init__(
143145
self.name = name
144146
self.version = version
145147
self.instructions = instructions
148+
self.website_url = website_url
149+
self.icons = icons
146150
self.lifespan = lifespan
147151
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
148152
types.PingRequest: _ping_handler,
@@ -176,6 +180,8 @@ def pkg_version(package: str) -> str:
176180
experimental_capabilities or {},
177181
),
178182
instructions=self.instructions,
183+
website_url=self.website_url,
184+
icons=self.icons,
179185
)
180186

181187
def get_capabilities(

0 commit comments

Comments
 (0)