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
12 changed files with 308 additions and 8 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue