"""Tests for icon support across all MCP object types.""" import pytest from mcp_types import Icon, IconTheme from fastmcp import Client, FastMCP from fastmcp.prompts import Message, Prompt from fastmcp.resources import Resource from fastmcp.resources.template import ResourceTemplate from fastmcp.tools import Tool class TestServerIcons: """Test icon support at the server/implementation level.""" async def test_server_with_icons_and_website_url(self): """Test that server accepts icons and websiteUrl in constructor.""" icons = [ Icon( src="https://example.com/icon.png", mime_type="image/png", sizes=["48x48"], ), Icon( src="data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=", mime_type="image/svg+xml", sizes=["any"], ), ] mcp = FastMCP( name="TestServer", version="1.0.0", website_url="https://example.com", icons=icons, ) # Verify that icons and website_url are passed to the underlying server async with Client(mcp) as client: server_info = client.session.server_info assert server_info.website_url == "https://example.com" assert server_info.icons == icons async def test_server_without_icons_and_website_url(self): """Test that server works without icons and websiteUrl.""" mcp = FastMCP(name="TestServer") async with Client(mcp) as client: server_info = client.session.server_info assert server_info.website_url is None assert server_info.icons is None class TestToolIcons: """Test icon support for tools.""" async def test_tool_with_icons(self): """Test that tools can have icons.""" mcp = FastMCP("TestServer") icons = [ Icon(src="https://example.com/tool-icon.png", mime_type="image/png"), ] @mcp.tool(icons=icons) def my_tool(name: str) -> str: """A tool with an icon.""" return f"Hello, {name}!" async with Client(mcp) as client: tools = await client.list_tools() assert len(tools) == 1 tool = tools[0] assert tool.icons == icons async def test_tool_from_function_with_icons(self): """Test creating a tool from a function with icons.""" icons = [Icon(src="https://example.com/icon.png")] def my_function(x: int) -> int: """A function.""" return x * 2 tool = Tool.from_function(my_function, icons=icons) assert tool.icons == icons # Verify it converts to MCP tool correctly mcp_tool = tool.to_mcp_tool() assert mcp_tool.icons == icons async def test_tool_without_icons(self): """Test that tools work without icons.""" mcp = FastMCP("TestServer") @mcp.tool def my_tool(name: str) -> str: """A tool without an icon.""" return f"Hello, {name}!" async with Client(mcp) as client: tools = await client.list_tools() assert len(tools) == 1 tool = tools[0] assert tool.icons is None class TestResourceIcons: """Test icon support for resources.""" async def test_resource_with_icons(self): """Test that resources can have icons.""" mcp = FastMCP("TestServer") icons = [Icon(src="https://example.com/resource-icon.png")] @mcp.resource("test://resource", icons=icons) def my_resource() -> str: """A resource with an icon.""" return "Resource content" async with Client(mcp) as client: resources = await client.list_resources() assert len(resources) == 1 resource = resources[0] assert resource.icons == icons async def test_resource_from_function_with_icons(self): """Test creating a resource from a function with icons.""" icons = [Icon(src="https://example.com/icon.png")] def my_function() -> str: """A function.""" return "content" resource = Resource.from_function( my_function, uri="test://resource", icons=icons, ) assert resource.icons == icons # Verify it converts to MCP resource correctly mcp_resource = resource.to_mcp_resource() assert mcp_resource.icons == icons async def test_resource_without_icons(self): """Test that resources work without icons.""" mcp = FastMCP("TestServer") @mcp.resource("test://resource") def my_resource() -> str: """A resource without an icon.""" return "Resource content" async with Client(mcp) as client: resources = await client.list_resources() assert len(resources) == 1 resource = resources[0] assert resource.icons is None class TestResourceTemplateIcons: """Test icon support for resource templates.""" async def test_resource_template_with_icons(self): """Test that resource templates can have icons.""" mcp = FastMCP("TestServer") icons = [Icon(src="https://example.com/template-icon.png")] @mcp.resource("test://resource/{id}", icons=icons) def my_template(id: str) -> str: """A resource template with an icon.""" return f"Resource {id}" async with Client(mcp) as client: templates = await client.list_resource_templates() assert len(templates) == 1 template = templates[0] assert template.icons == icons async def test_resource_template_from_function_with_icons(self): """Test creating a resource template from a function with icons.""" icons = [Icon(src="https://example.com/icon.png")] def my_function(id: str) -> str: """A function.""" return f"content-{id}" template = ResourceTemplate.from_function( my_function, uri_template="test://resource/{id}", icons=icons, ) assert template.icons == icons # Verify it converts to MCP template correctly mcp_template = template.to_mcp_template() assert mcp_template.icons == icons async def test_resource_template_without_icons(self): """Test that resource templates work without icons.""" mcp = FastMCP("TestServer") @mcp.resource("test://resource/{id}") def my_template(id: str) -> str: """A resource template without an icon.""" return f"Resource {id}" async with Client(mcp) as client: templates = await client.list_resource_templates() assert len(templates) == 1 template = templates[0] assert template.icons is None class TestPromptIcons: """Test icon support for prompts.""" async def test_prompt_with_icons(self): """Test that prompts can have icons.""" mcp = FastMCP("TestServer") icons = [Icon(src="https://example.com/prompt-icon.png")] @mcp.prompt(icons=icons) def my_prompt(name: str): """A prompt with an icon.""" return Message(f"Hello, {name}!") async with Client(mcp) as client: prompts = await client.list_prompts() assert len(prompts) == 1 prompt = prompts[0] assert prompt.icons == icons async def test_prompt_from_function_with_icons(self): """Test creating a prompt from a function with icons.""" icons = [Icon(src="https://example.com/icon.png")] def my_function(topic: str): """A function.""" return Message(f"Tell me about {topic}") prompt = Prompt.from_function(my_function, icons=icons) assert prompt.icons == icons # Verify it converts to MCP prompt correctly mcp_prompt = prompt.to_mcp_prompt() assert mcp_prompt.icons == icons async def test_prompt_without_icons(self): """Test that prompts work without icons.""" mcp = FastMCP("TestServer") @mcp.prompt def my_prompt(name: str): """A prompt without an icon.""" return Message(f"Hello, {name}!") async with Client(mcp) as client: prompts = await client.list_prompts() assert len(prompts) == 1 prompt = prompts[0] assert prompt.icons is None class TestIconTypes: """Test different types of icon data.""" async def test_multiple_icon_sizes(self): """Test that multiple icon sizes can be specified.""" icons = [ Icon( src="https://example.com/icon-48.png", mime_type="image/png", sizes=["48x48"], ), Icon( src="https://example.com/icon-96.png", mime_type="image/png", sizes=["96x96"], ), Icon( src="https://example.com/icon.svg", mime_type="image/svg+xml", sizes=["any"], ), ] mcp = FastMCP("TestServer", icons=icons) async with Client(mcp) as client: server_info = client.session.server_info assert len(server_info.icons) == 3 assert server_info.icons == icons async def test_data_uri_icon(self): """Test using data URIs for icons.""" # Simple SVG data URI data_uri = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+" icons = [Icon(src=data_uri, mime_type="image/svg+xml")] mcp = FastMCP("TestServer") @mcp.tool(icons=icons) def my_tool() -> str: """A tool with a data URI icon.""" return "result" async with Client(mcp) as client: tools = await client.list_tools() assert tools[0].icons[0].src == data_uri async def test_icon_without_optional_fields(self): """Test that icons work with only the src field.""" icons = [Icon(src="https://example.com/icon.png")] mcp = FastMCP("TestServer", icons=icons) async with Client(mcp) as client: server_info = client.session.server_info assert server_info.icons[0].src == "https://example.com/icon.png" assert server_info.icons[0].mime_type is None assert server_info.icons[0].sizes is None class TestIconTheme: """Test icon theme support.""" @pytest.mark.parametrize("theme", ["light", "dark"]) async def test_icon_with_theme_round_trips(self, theme: IconTheme): """Test that an icon's theme survives a client round-trip.""" icons = [Icon(src="https://example.com/icon.png", theme=theme)] mcp = FastMCP("TestServer", icons=icons) async with Client(mcp) as client: server_info = client.session.server_info assert server_info.icons[0].theme == theme async def test_icon_without_theme_is_none(self): """Test that an icon with no theme specified round-trips as None.""" icons = [Icon(src="https://example.com/icon.png")] mcp = FastMCP("TestServer", icons=icons) async with Client(mcp) as client: server_info = client.session.server_info assert server_info.icons[0].theme is None class TestIconImport: """Test that Icon must be imported from mcp_types.""" def test_icon_import(self): """Test that Icon must be imported from mcp.types, not fastmcp.""" # Icon should NOT be available from fastmcp import fastmcp assert not hasattr(fastmcp, "Icon") # Icon should be imported from mcp.types from mcp_types import Icon as MCPIcon icon = MCPIcon(src="https://example.com/icon.png") assert icon.src == "https://example.com/icon.png"