Implementation of SEP 973 - Additional metadata + icons support (#1357)
Some checks failed
Main branch checks / checks (push) Has been cancelled

This commit is contained in:
Peter Alexander 2025-09-24 20:29:04 +01:00 • committed by GitHub
commit 71889d7387
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 308 additions and 8 deletions

View file

@ -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"]

View file

@ -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,
)

View file

@ -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

View file

@ -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:

View file

@ -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,
)

View file

@ -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
]

View file

@ -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(

View file

@ -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)

View file

@ -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(

View file

@ -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

View file

@ -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,
)

View file

@ -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)