Add icons support for ResourceTemplate (#1412)
Some checks failed
Main branch checks / checks (push) Has been cancelled

This commit is contained in:
Peter Alexander 2025-10-01 13:23:46 +01:00 • committed by GitHub
commit 3e798bfc01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 28 additions and 2 deletions

View file

@ -10,6 +10,7 @@ from pydantic import AnyUrl
from mcp.server.fastmcp.resources.base import Resource from mcp.server.fastmcp.resources.base import Resource
from mcp.server.fastmcp.resources.templates import ResourceTemplate from mcp.server.fastmcp.resources.templates import ResourceTemplate
from mcp.server.fastmcp.utilities.logging import get_logger from mcp.server.fastmcp.utilities.logging import get_logger
from mcp.types import Icon
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context from mcp.server.fastmcp.server import Context
@ -61,6 +62,7 @@ class ResourceManager:
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,
) -> ResourceTemplate: ) -> ResourceTemplate:
"""Add a template from a function.""" """Add a template from a function."""
template = ResourceTemplate.from_function( template = ResourceTemplate.from_function(
@ -70,6 +72,7 @@ class ResourceManager:
title=title, title=title,
description=description, description=description,
mime_type=mime_type, mime_type=mime_type,
icons=icons,
) )
self._templates[template.uri_template] = template self._templates[template.uri_template] = template
return template return template

View file

@ -12,6 +12,7 @@ from pydantic import BaseModel, Field, validate_call
from mcp.server.fastmcp.resources.types import FunctionResource, Resource 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.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 Icon
if TYPE_CHECKING: if TYPE_CHECKING:
from mcp.server.fastmcp.server import Context 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) title: str | None = Field(description="Human-readable title of the resource", default=None)
description: str | None = Field(description="Description of what the resource does") 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") 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) fn: Callable[..., Any] = Field(exclude=True)
parameters: dict[str, Any] = Field(description="JSON schema for function parameters") 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") context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
@ -40,6 +42,7 @@ class ResourceTemplate(BaseModel):
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,
context_kwarg: str | None = None, context_kwarg: str | None = None,
) -> ResourceTemplate: ) -> ResourceTemplate:
"""Create a template from a function.""" """Create a template from a function."""
@ -67,6 +70,7 @@ class ResourceTemplate(BaseModel):
title=title, title=title,
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",
icons=icons,
fn=fn, fn=fn,
parameters=parameters, parameters=parameters,
context_kwarg=context_kwarg, context_kwarg=context_kwarg,
@ -103,7 +107,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 icons=self.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

@ -335,6 +335,7 @@ class FastMCP(Generic[LifespanResultT]):
title=template.title, title=template.title,
description=template.description, description=template.description,
mimeType=template.mime_type, mimeType=template.mime_type,
icons=template.icons,
) )
for template in templates for template in templates
] ]
@ -559,7 +560,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 icons=icons,
) )
else: else:
# Register as regular resource # Register as regular resource

View file

@ -470,6 +470,8 @@ class ResourceTemplate(BaseMetadata):
The MIME type for all resources that match this template. This should only be 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. 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 annotations: Annotations | None = None
meta: dict[str, Any] | None = Field(alias="_meta", default=None) meta: dict[str, Any] | None = Field(alias="_meta", default=None)
""" """

View file

@ -39,6 +39,12 @@ async def test_icons_and_website_url():
"""A test prompt with an icon.""" """A test prompt with an icon."""
return text 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 # Test server metadata includes websiteUrl and icons
assert mcp.name == "TestServer" assert mcp.name == "TestServer"
assert mcp.website_url == "https://example.com" assert mcp.website_url == "https://example.com"
@ -75,6 +81,16 @@ async def test_icons_and_website_url():
assert len(prompt.icons) == 1 assert len(prompt.icons) == 1
assert prompt.icons[0].src == test_icon.src 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(): async def test_multiple_icons():
"""Test that multiple icons can be added to tools, resources, and prompts.""" """Test that multiple icons can be added to tools, resources, and prompts."""