Skip to content

Commit 71889d7

Browse files
authored
Implementation of SEP 973 - Additional metadata + icons support (#1357)
1 parent b85e7bd commit 71889d7

File tree

16 files changed

+308
-8
lines changed

16 files changed

+308
-8
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,41 @@ def debug_error(error: str) -> list[base.Message]:
516516
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
517517
<!-- /snippet-source -->
518518

519+
### Icons
520+
521+
MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:
522+
523+
```python
524+
from mcp.server.fastmcp import FastMCP, Icon
525+
526+
# Create an icon from a file path or URL
527+
icon = Icon(
528+
src="icon.png",
529+
mimeType="image/png",
530+
sizes="64x64"
531+
)
532+
533+
# Add icons to server
534+
mcp = FastMCP(
535+
"My Server",
536+
website_url="https://example.com",
537+
icons=[icon]
538+
)
539+
540+
# Add icons to tools, resources, and prompts
541+
@mcp.tool(icons=[icon])
542+
def my_tool():
543+
"""Tool with an icon."""
544+
return "result"
545+
546+
@mcp.resource("demo://resource", icons=[icon])
547+
def my_resource():
548+
"""Resource with an icon."""
549+
return "content"
550+
```
551+
552+
_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/fastmcp/icons_demo.py)_
553+
519554
### Images
520555

521556
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
898933

899934
- `ctx.fastmcp.name` - The server's name as defined during initialization
900935
- `ctx.fastmcp.instructions` - Server instructions/description provided to clients
936+
- `ctx.fastmcp.website_url` - Optional website URL for the server
937+
- `ctx.fastmcp.icons` - Optional list of icons for UI display
901938
- `ctx.fastmcp.settings` - Complete server configuration object containing:
902939
- `debug` - Debug mode flag
903940
- `log_level` - Current logging level

examples/fastmcp/icons_demo.py

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

examples/fastmcp/mcp.png

2.53 KB
Loading

src/mcp/server/fastmcp/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from importlib.metadata import version
44

5+
from mcp.types import Icon
6+
57
from .server import Context, FastMCP
68
from .utilities.types import Audio, Image
79

810
__version__ = version("mcp")
9-
__all__ = ["FastMCP", "Context", "Image", "Audio"]
11+
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]

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

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

1212
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context
1313
from mcp.server.fastmcp.utilities.func_metadata import func_metadata
14-
from mcp.types import ContentBlock, TextContent
14+
from mcp.types import ContentBlock, Icon, TextContent
1515

1616
if TYPE_CHECKING:
1717
from mcp.server.fastmcp.server import Context
@@ -71,6 +71,7 @@ class Prompt(BaseModel):
7171
description: str | None = Field(None, description="Description of what the prompt does")
7272
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
7373
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
74+
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
7475
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
7576

7677
@classmethod
@@ -80,6 +81,7 @@ def from_function(
8081
name: str | None = None,
8182
title: str | None = None,
8283
description: str | None = None,
84+
icons: list[Icon] | None = None,
8385
context_kwarg: str | None = None,
8486
) -> Prompt:
8587
"""Create a Prompt from a function.
@@ -128,6 +130,7 @@ def from_function(
128130
description=description or fn.__doc__ or "",
129131
arguments=arguments,
130132
fn=fn,
133+
icons=icons,
131134
context_kwarg=context_kwarg,
132135
)
133136

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
@@ -103,6 +103,7 @@ async def create_resource(
103103
title=self.title,
104104
description=self.description,
105105
mime_type=self.mime_type,
106+
icons=None, # Resource templates don't support icons
106107
fn=lambda: result, # Capture result in closure
107108
)
108109
except Exception as 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: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
4444
from mcp.server.transport_security import TransportSecuritySettings
4545
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
46-
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, ToolAnnotations
46+
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, Icon, ToolAnnotations
4747
from mcp.types import Prompt as MCPPrompt
4848
from mcp.types import PromptArgument as MCPPromptArgument
4949
from mcp.types import Resource as MCPResource
@@ -120,10 +120,12 @@ async def wrap(_: MCPServer[LifespanResultT, Request]) -> AsyncIterator[Lifespan
120120

121121

122122
class FastMCP(Generic[LifespanResultT]):
123-
def __init__(
123+
def __init__( # noqa: PLR0913
124124
self,
125125
name: str | None = None,
126126
instructions: str | None = None,
127+
website_url: str | None = None,
128+
icons: list[Icon] | None = None,
127129
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,
128130
token_verifier: TokenVerifier | None = None,
129131
event_store: EventStore | None = None,
@@ -170,6 +172,8 @@ def __init__(
170172
self._mcp_server = MCPServer(
171173
name=name or "FastMCP",
172174
instructions=instructions,
175+
website_url=website_url,
176+
icons=icons,
173177
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
174178
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
175179
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
@@ -211,6 +215,14 @@ def name(self) -> str:
211215
def instructions(self) -> str | None:
212216
return self._mcp_server.instructions
213217

218+
@property
219+
def website_url(self) -> str | None:
220+
return self._mcp_server.website_url
221+
222+
@property
223+
def icons(self) -> list[Icon] | None:
224+
return self._mcp_server.icons
225+
214226
@property
215227
def session_manager(self) -> StreamableHTTPSessionManager:
216228
"""Get the StreamableHTTP session manager.
@@ -277,6 +289,7 @@ async def list_tools(self) -> list[MCPTool]:
277289
inputSchema=info.parameters,
278290
outputSchema=info.output_schema,
279291
annotations=info.annotations,
292+
icons=info.icons,
280293
)
281294
for info in tools
282295
]
@@ -308,6 +321,7 @@ async def list_resources(self) -> list[MCPResource]:
308321
title=resource.title,
309322
description=resource.description,
310323
mimeType=resource.mime_type,
324+
icons=resource.icons,
311325
)
312326
for resource in resources
313327
]
@@ -347,6 +361,7 @@ def add_tool(
347361
title: str | None = None,
348362
description: str | None = None,
349363
annotations: ToolAnnotations | None = None,
364+
icons: list[Icon] | None = None,
350365
structured_output: bool | None = None,
351366
) -> None:
352367
"""Add a tool to the server.
@@ -371,6 +386,7 @@ def add_tool(
371386
title=title,
372387
description=description,
373388
annotations=annotations,
389+
icons=icons,
374390
structured_output=structured_output,
375391
)
376392

@@ -380,6 +396,7 @@ def tool(
380396
title: str | None = None,
381397
description: str | None = None,
382398
annotations: ToolAnnotations | None = None,
399+
icons: list[Icon] | None = None,
383400
structured_output: bool | None = None,
384401
) -> Callable[[AnyFunction], AnyFunction]:
385402
"""Decorator to register a tool.
@@ -426,6 +443,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
426443
title=title,
427444
description=description,
428445
annotations=annotations,
446+
icons=icons,
429447
structured_output=structured_output,
430448
)
431449
return fn
@@ -466,6 +484,7 @@ def resource(
466484
title: str | None = None,
467485
description: str | None = None,
468486
mime_type: str | None = None,
487+
icons: list[Icon] | None = None,
469488
) -> Callable[[AnyFunction], AnyFunction]:
470489
"""Decorator to register a function as a resource.
471490
@@ -540,6 +559,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
540559
title=title,
541560
description=description,
542561
mime_type=mime_type,
562+
# Note: Resource templates don't support icons
543563
)
544564
else:
545565
# Register as regular resource
@@ -550,6 +570,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
550570
title=title,
551571
description=description,
552572
mime_type=mime_type,
573+
icons=icons,
553574
)
554575
self.add_resource(resource)
555576
return fn
@@ -565,7 +586,11 @@ def add_prompt(self, prompt: Prompt) -> None:
565586
self._prompt_manager.add_prompt(prompt)
566587

567588
def prompt(
568-
self, name: str | None = None, title: str | None = None, description: str | None = None
589+
self,
590+
name: str | None = None,
591+
title: str | None = None,
592+
description: str | None = None,
593+
icons: list[Icon] | None = None,
569594
) -> Callable[[AnyFunction], AnyFunction]:
570595
"""Decorator to register a prompt.
571596
@@ -609,7 +634,7 @@ async def analyze_file(path: str) -> list[Message]:
609634
)
610635

611636
def decorator(func: AnyFunction) -> AnyFunction:
612-
prompt = Prompt.from_function(func, name=name, title=title, description=description)
637+
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
613638
self.add_prompt(prompt)
614639
return func
615640

@@ -980,6 +1005,7 @@ async def list_prompts(self) -> list[MCPPrompt]:
9801005
)
9811006
for arg in (prompt.arguments or [])
9821007
],
1008+
icons=prompt.icons,
9831009
)
9841010
for prompt in prompts
9851011
]

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from mcp.server.fastmcp.exceptions import ToolError
1212
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter
1313
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
14-
from mcp.types import ToolAnnotations
14+
from mcp.types import Icon, ToolAnnotations
1515

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

3738
@cached_property
3839
def output_schema(self) -> dict[str, Any] | None:
@@ -47,6 +48,7 @@ def from_function(
4748
description: str | None = None,
4849
context_kwarg: str | None = None,
4950
annotations: ToolAnnotations | None = None,
51+
icons: list[Icon] | None = None,
5052
structured_output: bool | None = None,
5153
) -> Tool:
5254
"""Create a Tool from a function."""
@@ -78,6 +80,7 @@ def from_function(
7880
is_async=is_async,
7981
context_kwarg=context_kwarg,
8082
annotations=annotations,
83+
icons=icons,
8184
)
8285

8386
async def run(

0 commit comments

Comments
 (0)