Implementation of SEP 973 - Additional metadata + icons support (#1357)
Some checks failed
Main branch checks / checks (push) Has been cancelled
Some checks failed
Main branch checks / checks (push) Has been cancelled
This commit is contained in:
parent
b85e7bd1a9
commit
71889d7387
16 changed files with 308 additions and 8 deletions
37
README.md
37
README.md
|
|
@ -516,6 +516,41 @@ def debug_error(error: str) -> list[base.Message]:
|
|||
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
|
||||
<!-- /snippet-source -->
|
||||
|
||||
### Icons
|
||||
|
||||
MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP, Icon
|
||||
|
||||
# Create an icon from a file path or URL
|
||||
icon = Icon(
|
||||
src="icon.png",
|
||||
mimeType="image/png",
|
||||
sizes="64x64"
|
||||
)
|
||||
|
||||
# Add icons to server
|
||||
mcp = FastMCP(
|
||||
"My Server",
|
||||
website_url="https://example.com",
|
||||
icons=[icon]
|
||||
)
|
||||
|
||||
# Add icons to tools, resources, and prompts
|
||||
@mcp.tool(icons=[icon])
|
||||
def my_tool():
|
||||
"""Tool with an icon."""
|
||||
return "result"
|
||||
|
||||
@mcp.resource("demo://resource", icons=[icon])
|
||||
def my_resource():
|
||||
"""Resource with an icon."""
|
||||
return "content"
|
||||
```
|
||||
|
||||
_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/fastmcp/icons_demo.py)_
|
||||
|
||||
### Images
|
||||
|
||||
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
|
|||
|
||||
- `ctx.fastmcp.name` - The server's name as defined during initialization
|
||||
- `ctx.fastmcp.instructions` - Server instructions/description provided to clients
|
||||
- `ctx.fastmcp.website_url` - Optional website URL for the server
|
||||
- `ctx.fastmcp.icons` - Optional list of icons for UI display
|
||||
- `ctx.fastmcp.settings` - Complete server configuration object containing:
|
||||
- `debug` - Debug mode flag
|
||||
- `log_level` - Current logging level
|
||||
|
|
|
|||
55
examples/fastmcp/icons_demo.py
Normal file
55
examples/fastmcp/icons_demo.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""
|
||||
FastMCP Icons Demo Server
|
||||
|
||||
Demonstrates using icons with tools, resources, prompts, and implementation.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
from mcp.server.fastmcp import FastMCP, Icon
|
||||
|
||||
# Load the icon file and convert to data URI
|
||||
icon_path = Path(__file__).parent / "mcp.png"
|
||||
icon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()
|
||||
icon_data_uri = f"data:image/png;base64,{icon_data}"
|
||||
|
||||
icon_data = Icon(src=icon_data_uri, mimeType="image/png", sizes="64x64")
|
||||
|
||||
# Create server with icons in implementation
|
||||
mcp = FastMCP("Icons Demo Server", website_url="https://github.com/modelcontextprotocol/python-sdk", icons=[icon_data])
|
||||
|
||||
|
||||
@mcp.tool(icons=[icon_data])
|
||||
def demo_tool(message: str) -> str:
|
||||
"""A demo tool with an icon."""
|
||||
return message
|
||||
|
||||
|
||||
@mcp.resource("demo://readme", icons=[icon_data])
|
||||
def readme_resource() -> str:
|
||||
"""A demo resource with an icon"""
|
||||
return "This resource has an icon"
|
||||
|
||||
|
||||
@mcp.prompt("prompt_with_icon", icons=[icon_data])
|
||||
def prompt_with_icon(text: str) -> str:
|
||||
"""A demo prompt with an icon"""
|
||||
return text
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
icons=[
|
||||
Icon(src=icon_data_uri, mimeType="image/png", sizes="16x16"),
|
||||
Icon(src=icon_data_uri, mimeType="image/png", sizes="32x32"),
|
||||
Icon(src=icon_data_uri, mimeType="image/png", sizes="64x64"),
|
||||
]
|
||||
)
|
||||
def multi_icon_tool(action: str) -> str:
|
||||
"""A tool demonstrating multiple icons."""
|
||||
return "multi_icon_tool"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the server
|
||||
mcp.run()
|
||||
BIN
examples/fastmcp/mcp.png
Normal file
BIN
examples/fastmcp/mcp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
from importlib.metadata import version
|
||||
|
||||
from mcp.types import Icon
|
||||
|
||||
from .server import Context, FastMCP
|
||||
from .utilities.types import Audio, Image
|
||||
|
||||
__version__ = version("mcp")
|
||||
__all__ = ["FastMCP", "Context", "Image", "Audio"]
|
||||
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from pydantic import BaseModel, Field, TypeAdapter, validate_call
|
|||
|
||||
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 ContentBlock, TextContent
|
||||
from mcp.types import ContentBlock, Icon, TextContent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.fastmcp.server import Context
|
||||
|
|
@ -71,6 +71,7 @@ class Prompt(BaseModel):
|
|||
description: str | None = Field(None, description="Description of what the prompt does")
|
||||
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
|
||||
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
|
||||
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
|
||||
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -80,6 +81,7 @@ class Prompt(BaseModel):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
context_kwarg: str | None = None,
|
||||
) -> Prompt:
|
||||
"""Create a Prompt from a function.
|
||||
|
|
@ -128,6 +130,7 @@ class Prompt(BaseModel):
|
|||
description=description or fn.__doc__ or "",
|
||||
arguments=arguments,
|
||||
fn=fn,
|
||||
icons=icons,
|
||||
context_kwarg=context_kwarg,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from pydantic import (
|
|||
field_validator,
|
||||
)
|
||||
|
||||
from mcp.types import Icon
|
||||
|
||||
|
||||
class Resource(BaseModel, abc.ABC):
|
||||
"""Base class for all resources."""
|
||||
|
|
@ -28,6 +30,7 @@ class Resource(BaseModel, abc.ABC):
|
|||
description="MIME type of the resource content",
|
||||
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
||||
)
|
||||
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this resource")
|
||||
|
||||
@field_validator("name", mode="before")
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ class ResourceTemplate(BaseModel):
|
|||
title=self.title,
|
||||
description=self.description,
|
||||
mime_type=self.mime_type,
|
||||
icons=None, # Resource templates don't support icons
|
||||
fn=lambda: result, # Capture result in closure
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import pydantic_core
|
|||
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
|
||||
|
||||
from mcp.server.fastmcp.resources.base import Resource
|
||||
from mcp.types import Icon
|
||||
|
||||
|
||||
class TextResource(Resource):
|
||||
|
|
@ -80,6 +81,7 @@ class FunctionResource(Resource):
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
mime_type: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
) -> "FunctionResource":
|
||||
"""Create a FunctionResource from a function."""
|
||||
func_name = name or fn.__name__
|
||||
|
|
@ -96,6 +98,7 @@ class FunctionResource(Resource):
|
|||
description=description or fn.__doc__ or "",
|
||||
mime_type=mime_type or "text/plain",
|
||||
fn=fn,
|
||||
icons=icons,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ from mcp.server.streamable_http import EventStore
|
|||
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
||||
from mcp.server.transport_security import TransportSecuritySettings
|
||||
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
|
||||
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, ToolAnnotations
|
||||
from mcp.types import AnyFunction, ContentBlock, GetPromptResult, Icon, ToolAnnotations
|
||||
from mcp.types import Prompt as MCPPrompt
|
||||
from mcp.types import PromptArgument as MCPPromptArgument
|
||||
from mcp.types import Resource as MCPResource
|
||||
|
|
@ -120,10 +120,12 @@ def lifespan_wrapper(
|
|||
|
||||
|
||||
class FastMCP(Generic[LifespanResultT]):
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
name: str | None = None,
|
||||
instructions: str | None = None,
|
||||
website_url: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,
|
||||
token_verifier: TokenVerifier | None = None,
|
||||
event_store: EventStore | None = None,
|
||||
|
|
@ -170,6 +172,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._mcp_server = MCPServer(
|
||||
name=name or "FastMCP",
|
||||
instructions=instructions,
|
||||
website_url=website_url,
|
||||
icons=icons,
|
||||
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
|
||||
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
|
||||
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
|
||||
|
|
@ -211,6 +215,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
def instructions(self) -> str | None:
|
||||
return self._mcp_server.instructions
|
||||
|
||||
@property
|
||||
def website_url(self) -> str | None:
|
||||
return self._mcp_server.website_url
|
||||
|
||||
@property
|
||||
def icons(self) -> list[Icon] | None:
|
||||
return self._mcp_server.icons
|
||||
|
||||
@property
|
||||
def session_manager(self) -> StreamableHTTPSessionManager:
|
||||
"""Get the StreamableHTTP session manager.
|
||||
|
|
@ -277,6 +289,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
inputSchema=info.parameters,
|
||||
outputSchema=info.output_schema,
|
||||
annotations=info.annotations,
|
||||
icons=info.icons,
|
||||
)
|
||||
for info in tools
|
||||
]
|
||||
|
|
@ -308,6 +321,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title=resource.title,
|
||||
description=resource.description,
|
||||
mimeType=resource.mime_type,
|
||||
icons=resource.icons,
|
||||
)
|
||||
for resource in resources
|
||||
]
|
||||
|
|
@ -347,6 +361,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
structured_output: bool | None = None,
|
||||
) -> None:
|
||||
"""Add a tool to the server.
|
||||
|
|
@ -371,6 +386,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title=title,
|
||||
description=description,
|
||||
annotations=annotations,
|
||||
icons=icons,
|
||||
structured_output=structured_output,
|
||||
)
|
||||
|
||||
|
|
@ -380,6 +396,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
structured_output: bool | None = None,
|
||||
) -> Callable[[AnyFunction], AnyFunction]:
|
||||
"""Decorator to register a tool.
|
||||
|
|
@ -426,6 +443,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title=title,
|
||||
description=description,
|
||||
annotations=annotations,
|
||||
icons=icons,
|
||||
structured_output=structured_output,
|
||||
)
|
||||
return fn
|
||||
|
|
@ -466,6 +484,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
mime_type: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
) -> Callable[[AnyFunction], AnyFunction]:
|
||||
"""Decorator to register a function as a resource.
|
||||
|
||||
|
|
@ -540,6 +559,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title=title,
|
||||
description=description,
|
||||
mime_type=mime_type,
|
||||
# Note: Resource templates don't support icons
|
||||
)
|
||||
else:
|
||||
# Register as regular resource
|
||||
|
|
@ -550,6 +570,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
title=title,
|
||||
description=description,
|
||||
mime_type=mime_type,
|
||||
icons=icons,
|
||||
)
|
||||
self.add_resource(resource)
|
||||
return fn
|
||||
|
|
@ -565,7 +586,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
self._prompt_manager.add_prompt(prompt)
|
||||
|
||||
def prompt(
|
||||
self, name: str | None = None, title: str | None = None, description: str | None = None
|
||||
self,
|
||||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
) -> Callable[[AnyFunction], AnyFunction]:
|
||||
"""Decorator to register a prompt.
|
||||
|
||||
|
|
@ -609,7 +634,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
|
||||
def decorator(func: AnyFunction) -> AnyFunction:
|
||||
prompt = Prompt.from_function(func, name=name, title=title, description=description)
|
||||
prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)
|
||||
self.add_prompt(prompt)
|
||||
return func
|
||||
|
||||
|
|
@ -980,6 +1005,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
)
|
||||
for arg in (prompt.arguments or [])
|
||||
],
|
||||
icons=prompt.icons,
|
||||
)
|
||||
for prompt in prompts
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
|
|||
from mcp.server.fastmcp.exceptions import ToolError
|
||||
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter
|
||||
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
||||
from mcp.types import ToolAnnotations
|
||||
from mcp.types import Icon, ToolAnnotations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.fastmcp.server import Context
|
||||
|
|
@ -33,6 +33,7 @@ class Tool(BaseModel):
|
|||
is_async: bool = Field(description="Whether the tool is async")
|
||||
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context")
|
||||
annotations: ToolAnnotations | None = Field(None, description="Optional annotations for the tool")
|
||||
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this tool")
|
||||
|
||||
@cached_property
|
||||
def output_schema(self) -> dict[str, Any] | None:
|
||||
|
|
@ -47,6 +48,7 @@ class Tool(BaseModel):
|
|||
description: str | None = None,
|
||||
context_kwarg: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
structured_output: bool | None = None,
|
||||
) -> Tool:
|
||||
"""Create a Tool from a function."""
|
||||
|
|
@ -78,6 +80,7 @@ class Tool(BaseModel):
|
|||
is_async=is_async,
|
||||
context_kwarg=context_kwarg,
|
||||
annotations=annotations,
|
||||
icons=icons,
|
||||
)
|
||||
|
||||
async def run(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from mcp.server.fastmcp.exceptions import ToolError
|
|||
from mcp.server.fastmcp.tools.base import Tool
|
||||
from mcp.server.fastmcp.utilities.logging import get_logger
|
||||
from mcp.shared.context import LifespanContextT, RequestT
|
||||
from mcp.types import ToolAnnotations
|
||||
from mcp.types import Icon, ToolAnnotations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.fastmcp.server import Context
|
||||
|
|
@ -49,6 +49,7 @@ class ToolManager:
|
|||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
structured_output: bool | None = None,
|
||||
) -> Tool:
|
||||
"""Add a tool to the server."""
|
||||
|
|
@ -58,6 +59,7 @@ class ToolManager:
|
|||
title=title,
|
||||
description=description,
|
||||
annotations=annotations,
|
||||
icons=icons,
|
||||
structured_output=structured_output,
|
||||
)
|
||||
existing = self._tools.get(tool.name)
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
name: str,
|
||||
version: str | None = None,
|
||||
instructions: str | None = None,
|
||||
website_url: str | None = None,
|
||||
icons: list[types.Icon] | None = None,
|
||||
lifespan: Callable[
|
||||
[Server[LifespanResultT, RequestT]],
|
||||
AbstractAsyncContextManager[LifespanResultT],
|
||||
|
|
@ -144,6 +146,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
self.name = name
|
||||
self.version = version
|
||||
self.instructions = instructions
|
||||
self.website_url = website_url
|
||||
self.icons = icons
|
||||
self.lifespan = lifespan
|
||||
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
|
||||
types.PingRequest: _ping_handler,
|
||||
|
|
@ -177,6 +181,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
|||
experimental_capabilities or {},
|
||||
),
|
||||
instructions=self.instructions,
|
||||
website_url=self.website_url,
|
||||
icons=self.icons,
|
||||
)
|
||||
|
||||
def get_capabilities(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ and tools.
|
|||
from pydantic import BaseModel
|
||||
|
||||
from mcp.types import (
|
||||
Icon,
|
||||
ServerCapabilities,
|
||||
)
|
||||
|
||||
|
|
@ -15,3 +16,5 @@ class InitializationOptions(BaseModel):
|
|||
server_version: str
|
||||
capabilities: ServerCapabilities
|
||||
instructions: str | None = None
|
||||
website_url: str | None = None
|
||||
icons: list[Icon] | None = None
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ class ServerSession(
|
|||
serverInfo=types.Implementation(
|
||||
name=self._init_options.server_name,
|
||||
version=self._init_options.server_version,
|
||||
websiteUrl=self._init_options.website_url,
|
||||
icons=self._init_options.icons,
|
||||
),
|
||||
instructions=self._init_options.instructions,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -213,10 +213,32 @@ class BaseMetadata(BaseModel):
|
|||
"""
|
||||
|
||||
|
||||
class Icon(BaseModel):
|
||||
"""An icon for display in user interfaces."""
|
||||
|
||||
src: str
|
||||
"""URL or data URI for the icon."""
|
||||
|
||||
mimeType: str | None = None
|
||||
"""Optional MIME type for the icon."""
|
||||
|
||||
sizes: str | None = None
|
||||
"""Optional string specifying icon dimensions (e.g., "48x48 96x96")."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class Implementation(BaseMetadata):
|
||||
"""Describes the name and version of an MCP implementation."""
|
||||
|
||||
version: str
|
||||
|
||||
websiteUrl: str | None = None
|
||||
"""An optional URL of the website for this implementation."""
|
||||
|
||||
icons: list[Icon] | None = None
|
||||
"""An optional list of icons for this implementation."""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
|
|
@ -422,6 +444,8 @@ class Resource(BaseMetadata):
|
|||
|
||||
This can be used by Hosts to display file sizes and estimate context window usage.
|
||||
"""
|
||||
icons: list[Icon] | None = None
|
||||
"""An optional list of icons for this resource."""
|
||||
annotations: Annotations | None = None
|
||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
||||
"""
|
||||
|
|
@ -628,6 +652,8 @@ class Prompt(BaseMetadata):
|
|||
"""An optional description of what this prompt provides."""
|
||||
arguments: list[PromptArgument] | None = None
|
||||
"""A list of arguments to use for templating the prompt."""
|
||||
icons: list[Icon] | None = None
|
||||
"""An optional list of icons for this prompt."""
|
||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
||||
"""
|
||||
See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)
|
||||
|
|
@ -852,6 +878,8 @@ class Tool(BaseMetadata):
|
|||
An optional JSON Schema object defining the structure of the tool's output
|
||||
returned in the structuredContent field of a CallToolResult.
|
||||
"""
|
||||
icons: list[Icon] | None = None
|
||||
"""An optional list of icons for this tool."""
|
||||
annotations: ToolAnnotations | None = None
|
||||
"""Optional additional tool information."""
|
||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
||||
|
|
|
|||
126
tests/issues/test_1338_icons_and_metadata.py
Normal file
126
tests/issues/test_1338_icons_and_metadata.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""Test icon and metadata support (SEP-973)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp.types import Icon
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def test_icons_and_website_url():
|
||||
"""Test that icons and websiteUrl are properly returned in API calls."""
|
||||
|
||||
# Create test icon
|
||||
test_icon = Icon(
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
|
||||
mimeType="image/png",
|
||||
sizes="1x1",
|
||||
)
|
||||
|
||||
# Create server with website URL and icon
|
||||
mcp = FastMCP("TestServer", website_url="https://example.com", icons=[test_icon])
|
||||
|
||||
# Create tool with icon
|
||||
@mcp.tool(icons=[test_icon])
|
||||
def test_tool(message: str) -> str:
|
||||
"""A test tool with an icon."""
|
||||
return message
|
||||
|
||||
# Create resource with icon
|
||||
@mcp.resource("test://resource", icons=[test_icon])
|
||||
def test_resource() -> str:
|
||||
"""A test resource with an icon."""
|
||||
return "test content"
|
||||
|
||||
# Create prompt with icon
|
||||
@mcp.prompt("test_prompt", icons=[test_icon])
|
||||
def test_prompt(text: str) -> str:
|
||||
"""A test prompt with an icon."""
|
||||
return text
|
||||
|
||||
# Test server metadata includes websiteUrl and icons
|
||||
assert mcp.name == "TestServer"
|
||||
assert mcp.website_url == "https://example.com"
|
||||
assert mcp.icons is not None
|
||||
assert len(mcp.icons) == 1
|
||||
assert mcp.icons[0].src == test_icon.src
|
||||
assert mcp.icons[0].mimeType == test_icon.mimeType
|
||||
assert mcp.icons[0].sizes == test_icon.sizes
|
||||
|
||||
# Test tool includes icon
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
assert tool.name == "test_tool"
|
||||
assert tool.icons is not None
|
||||
assert len(tool.icons) == 1
|
||||
assert tool.icons[0].src == test_icon.src
|
||||
|
||||
# Test resource includes icon
|
||||
resources = await mcp.list_resources()
|
||||
assert len(resources) == 1
|
||||
resource = resources[0]
|
||||
assert str(resource.uri) == "test://resource"
|
||||
assert resource.icons is not None
|
||||
assert len(resource.icons) == 1
|
||||
assert resource.icons[0].src == test_icon.src
|
||||
|
||||
# Test prompt includes icon
|
||||
prompts = await mcp.list_prompts()
|
||||
assert len(prompts) == 1
|
||||
prompt = prompts[0]
|
||||
assert prompt.name == "test_prompt"
|
||||
assert prompt.icons is not None
|
||||
assert len(prompt.icons) == 1
|
||||
assert prompt.icons[0].src == test_icon.src
|
||||
|
||||
|
||||
async def test_multiple_icons():
|
||||
"""Test that multiple icons can be added to tools, resources, and prompts."""
|
||||
|
||||
# Create multiple test icons
|
||||
icon1 = Icon(src="data:image/png;base64,icon1", mimeType="image/png", sizes="16x16")
|
||||
icon2 = Icon(src="data:image/png;base64,icon2", mimeType="image/png", sizes="32x32")
|
||||
icon3 = Icon(src="data:image/png;base64,icon3", mimeType="image/png", sizes="64x64")
|
||||
|
||||
mcp = FastMCP("MultiIconServer")
|
||||
|
||||
# Create tool with multiple icons
|
||||
@mcp.tool(icons=[icon1, icon2, icon3])
|
||||
def multi_icon_tool() -> str:
|
||||
"""A tool with multiple icons."""
|
||||
return "success"
|
||||
|
||||
# Test tool has all icons
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
assert tool.icons is not None
|
||||
assert len(tool.icons) == 3
|
||||
assert tool.icons[0].sizes == "16x16"
|
||||
assert tool.icons[1].sizes == "32x32"
|
||||
assert tool.icons[2].sizes == "64x64"
|
||||
|
||||
|
||||
async def test_no_icons_or_website():
|
||||
"""Test that server works without icons or websiteUrl."""
|
||||
|
||||
mcp = FastMCP("BasicServer")
|
||||
|
||||
@mcp.tool()
|
||||
def basic_tool() -> str:
|
||||
"""A basic tool without icons."""
|
||||
return "success"
|
||||
|
||||
# Test server metadata has no websiteUrl or icons
|
||||
assert mcp.name == "BasicServer"
|
||||
assert mcp.website_url is None
|
||||
assert mcp.icons is None
|
||||
|
||||
# Test tool has no icons
|
||||
tools = await mcp.list_tools()
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
assert tool.name == "basic_tool"
|
||||
assert tool.icons is None
|
||||
Loading…
Reference in a new issue