From 07558fc6b8cde8b16a94d085207cb881cf43925d Mon Sep 17 00:00:00 2001 From: Peter Alexander Date: Mon, 29 Sep 2025 15:20:31 +0100 Subject: [PATCH 1/2] Add icons support for ResourceTemplate This change adds icons metadata support to ResourceTemplate, bringing it in line with Resource, Tool, and Prompt types. The MCP specification was updated to include icons for ResourceTemplate, and this implements that feature in the Python SDK. Changes: - Add icons field to ResourceTemplate type definition - Update ResourceTemplate class to store and pass icons - Update resource_manager to accept icons parameter - Update FastMCP server to expose icons in list_resource_templates - Add test coverage for resource template icons All primitives (tools, resources, prompts, resource templates) now consistently support icons metadata. --- .../server/fastmcp/resources/resource_manager.py | 2 ++ src/mcp/server/fastmcp/resources/templates.py | 6 +++++- src/mcp/server/fastmcp/server.py | 3 ++- src/mcp/types.py | 2 ++ tests/issues/test_1338_icons_and_metadata.py | 16 ++++++++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/fastmcp/resources/resource_manager.py b/src/mcp/server/fastmcp/resources/resource_manager.py index ad559bd5c..aeb9f6bf4 100644 --- a/src/mcp/server/fastmcp/resources/resource_manager.py +++ b/src/mcp/server/fastmcp/resources/resource_manager.py @@ -61,6 +61,7 @@ def add_template( title: str | None = None, description: str | None = None, mime_type: str | None = None, + icons: list[Any] | None = None, ) -> ResourceTemplate: """Add a template from a function.""" template = ResourceTemplate.from_function( @@ -70,6 +71,7 @@ def add_template( title=title, description=description, mime_type=mime_type, + icons=icons, ) self._templates[template.uri_template] = template return template diff --git a/src/mcp/server/fastmcp/resources/templates.py b/src/mcp/server/fastmcp/resources/templates.py index 922364cd8..8b5af2574 100644 --- a/src/mcp/server/fastmcp/resources/templates.py +++ b/src/mcp/server/fastmcp/resources/templates.py @@ -12,6 +12,7 @@ from mcp.server.fastmcp.resources.types import FunctionResource, Resource 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.types import Icon if TYPE_CHECKING: from mcp.server.fastmcp.server import Context @@ -27,6 +28,7 @@ class ResourceTemplate(BaseModel): title: str | None = Field(description="Human-readable title of the resource", default=None) description: str | None = Field(description="Description of what the resource does") mime_type: str = Field(default="text/plain", description="MIME type of the resource content") + icons: list[Icon] | None = Field(default=None, description="Optional list of icons for the resource template") fn: Callable[..., Any] = Field(exclude=True) parameters: dict[str, Any] = Field(description="JSON schema for function parameters") context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context") @@ -40,6 +42,7 @@ def from_function( title: str | None = None, description: str | None = None, mime_type: str | None = None, + icons: list[Icon] | None = None, context_kwarg: str | None = None, ) -> ResourceTemplate: """Create a template from a function.""" @@ -67,6 +70,7 @@ def from_function( title=title, description=description or fn.__doc__ or "", mime_type=mime_type or "text/plain", + icons=icons, fn=fn, parameters=parameters, context_kwarg=context_kwarg, @@ -103,7 +107,7 @@ async def create_resource( title=self.title, description=self.description, mime_type=self.mime_type, - icons=None, # Resource templates don't support icons + icons=self.icons, fn=lambda: result, # Capture result in closure ) except Exception as e: diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 839632930..c2dbc4436 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -335,6 +335,7 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]: title=template.title, description=template.description, mimeType=template.mime_type, + icons=template.icons, ) for template in templates ] @@ -559,7 +560,7 @@ def decorator(fn: AnyFunction) -> AnyFunction: title=title, description=description, mime_type=mime_type, - # Note: Resource templates don't support icons + icons=icons, ) else: # Register as regular resource diff --git a/src/mcp/types.py b/src/mcp/types.py index 0cf175584..871322740 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -470,6 +470,8 @@ class ResourceTemplate(BaseMetadata): The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type. """ + icons: list[Icon] | None = None + """An optional list of icons for this resource template.""" annotations: Annotations | None = None meta: dict[str, Any] | None = Field(alias="_meta", default=None) """ diff --git a/tests/issues/test_1338_icons_and_metadata.py b/tests/issues/test_1338_icons_and_metadata.py index 3d4f9b868..8a9897fcf 100644 --- a/tests/issues/test_1338_icons_and_metadata.py +++ b/tests/issues/test_1338_icons_and_metadata.py @@ -39,6 +39,12 @@ def test_prompt(text: str) -> str: """A test prompt with an icon.""" return text + # Create resource template with icon + @mcp.resource("test://weather/{city}", icons=[test_icon]) + def test_resource_template(city: str) -> str: + """Get weather for a city.""" + return f"Weather for {city}" + # Test server metadata includes websiteUrl and icons assert mcp.name == "TestServer" assert mcp.website_url == "https://example.com" @@ -75,6 +81,16 @@ def test_prompt(text: str) -> str: assert len(prompt.icons) == 1 assert prompt.icons[0].src == test_icon.src + # Test resource template includes icon + templates = await mcp.list_resource_templates() + assert len(templates) == 1 + template = templates[0] + assert template.name == "test_resource_template" + assert template.uriTemplate == "test://weather/{city}" + assert template.icons is not None + assert len(template.icons) == 1 + assert template.icons[0].src == test_icon.src + async def test_multiple_icons(): """Test that multiple icons can be added to tools, resources, and prompts.""" From a91a983fb93693d95f0bd04033067bb308ecf110 Mon Sep 17 00:00:00 2001 From: Peter Alexander Date: Wed, 1 Oct 2025 12:32:30 +0100 Subject: [PATCH 2/2] Use Icon type instead of Any in resource_manager --- src/mcp/server/fastmcp/resources/resource_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/fastmcp/resources/resource_manager.py b/src/mcp/server/fastmcp/resources/resource_manager.py index aeb9f6bf4..b2865def8 100644 --- a/src/mcp/server/fastmcp/resources/resource_manager.py +++ b/src/mcp/server/fastmcp/resources/resource_manager.py @@ -10,6 +10,7 @@ from mcp.server.fastmcp.resources.base import Resource from mcp.server.fastmcp.resources.templates import ResourceTemplate from mcp.server.fastmcp.utilities.logging import get_logger +from mcp.types import Icon if TYPE_CHECKING: from mcp.server.fastmcp.server import Context @@ -61,7 +62,7 @@ def add_template( title: str | None = None, description: str | None = None, mime_type: str | None = None, - icons: list[Any] | None = None, + icons: list[Icon] | None = None, ) -> ResourceTemplate: """Add a template from a function.""" template = ResourceTemplate.from_function(