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)_
|
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
|
||||||
<!-- /snippet-source -->
|
<!-- /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
|
### Images
|
||||||
|
|
||||||
FastMCP provides an `Image` class that automatically handles image data:
|
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.name` - The server's name as defined during initialization
|
||||||
- `ctx.fastmcp.instructions` - Server instructions/description provided to clients
|
- `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:
|
- `ctx.fastmcp.settings` - Complete server configuration object containing:
|
||||||
- `debug` - Debug mode flag
|
- `debug` - Debug mode flag
|
||||||
- `log_level` - Current logging level
|
- `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 importlib.metadata import version
|
||||||
|
|
||||||
|
from mcp.types import Icon
|
||||||
|
|
||||||
from .server import Context, FastMCP
|
from .server import Context, FastMCP
|
||||||
from .utilities.types import Audio, Image
|
from .utilities.types import Audio, Image
|
||||||
|
|
||||||
__version__ = version("mcp")
|
__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.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 ContentBlock, TextContent
|
from mcp.types import ContentBlock, Icon, TextContent
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mcp.server.fastmcp.server import Context
|
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")
|
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")
|
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
|
||||||
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
|
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)
|
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -80,6 +81,7 @@ class Prompt(BaseModel):
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
|
icons: list[Icon] | None = None,
|
||||||
context_kwarg: str | None = None,
|
context_kwarg: str | None = None,
|
||||||
) -> Prompt:
|
) -> Prompt:
|
||||||
"""Create a Prompt from a function.
|
"""Create a Prompt from a function.
|
||||||
|
|
@ -128,6 +130,7 @@ class Prompt(BaseModel):
|
||||||
description=description or fn.__doc__ or "",
|
description=description or fn.__doc__ or "",
|
||||||
arguments=arguments,
|
arguments=arguments,
|
||||||
fn=fn,
|
fn=fn,
|
||||||
|
icons=icons,
|
||||||
context_kwarg=context_kwarg,
|
context_kwarg=context_kwarg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ from pydantic import (
|
||||||
field_validator,
|
field_validator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from mcp.types import Icon
|
||||||
|
|
||||||
|
|
||||||
class Resource(BaseModel, abc.ABC):
|
class Resource(BaseModel, abc.ABC):
|
||||||
"""Base class for all resources."""
|
"""Base class for all resources."""
|
||||||
|
|
@ -28,6 +30,7 @@ class Resource(BaseModel, abc.ABC):
|
||||||
description="MIME type of the resource content",
|
description="MIME type of the resource content",
|
||||||
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
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")
|
@field_validator("name", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,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
|
||||||
fn=lambda: result, # Capture result in closure
|
fn=lambda: result, # Capture result in closure
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import pydantic_core
|
||||||
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
|
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
|
||||||
|
|
||||||
from mcp.server.fastmcp.resources.base import Resource
|
from mcp.server.fastmcp.resources.base import Resource
|
||||||
|
from mcp.types import Icon
|
||||||
|
|
||||||
|
|
||||||
class TextResource(Resource):
|
class TextResource(Resource):
|
||||||
|
|
@ -80,6 +81,7 @@ class FunctionResource(Resource):
|
||||||
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,
|
||||||
) -> "FunctionResource":
|
) -> "FunctionResource":
|
||||||
"""Create a FunctionResource from a function."""
|
"""Create a FunctionResource from a function."""
|
||||||
func_name = name or fn.__name__
|
func_name = name or fn.__name__
|
||||||
|
|
@ -96,6 +98,7 @@ class FunctionResource(Resource):
|
||||||
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",
|
||||||
fn=fn,
|
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.streamable_http_manager import StreamableHTTPSessionManager
|
||||||
from mcp.server.transport_security import TransportSecuritySettings
|
from mcp.server.transport_security import TransportSecuritySettings
|
||||||
from mcp.shared.context import LifespanContextT, RequestContext, RequestT
|
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 Prompt as MCPPrompt
|
||||||
from mcp.types import PromptArgument as MCPPromptArgument
|
from mcp.types import PromptArgument as MCPPromptArgument
|
||||||
from mcp.types import Resource as MCPResource
|
from mcp.types import Resource as MCPResource
|
||||||
|
|
@ -120,10 +120,12 @@ def lifespan_wrapper(
|
||||||
|
|
||||||
|
|
||||||
class FastMCP(Generic[LifespanResultT]):
|
class FastMCP(Generic[LifespanResultT]):
|
||||||
def __init__(
|
def __init__( # noqa: PLR0913
|
||||||
self,
|
self,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
instructions: 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,
|
auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,
|
||||||
token_verifier: TokenVerifier | None = None,
|
token_verifier: TokenVerifier | None = None,
|
||||||
event_store: EventStore | None = None,
|
event_store: EventStore | None = None,
|
||||||
|
|
@ -170,6 +172,8 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
self._mcp_server = MCPServer(
|
self._mcp_server = MCPServer(
|
||||||
name=name or "FastMCP",
|
name=name or "FastMCP",
|
||||||
instructions=instructions,
|
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.
|
# 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.
|
# 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
|
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:
|
def instructions(self) -> str | None:
|
||||||
return self._mcp_server.instructions
|
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
|
@property
|
||||||
def session_manager(self) -> StreamableHTTPSessionManager:
|
def session_manager(self) -> StreamableHTTPSessionManager:
|
||||||
"""Get the StreamableHTTP session manager.
|
"""Get the StreamableHTTP session manager.
|
||||||
|
|
@ -277,6 +289,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
inputSchema=info.parameters,
|
inputSchema=info.parameters,
|
||||||
outputSchema=info.output_schema,
|
outputSchema=info.output_schema,
|
||||||
annotations=info.annotations,
|
annotations=info.annotations,
|
||||||
|
icons=info.icons,
|
||||||
)
|
)
|
||||||
for info in tools
|
for info in tools
|
||||||
]
|
]
|
||||||
|
|
@ -308,6 +321,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
title=resource.title,
|
title=resource.title,
|
||||||
description=resource.description,
|
description=resource.description,
|
||||||
mimeType=resource.mime_type,
|
mimeType=resource.mime_type,
|
||||||
|
icons=resource.icons,
|
||||||
)
|
)
|
||||||
for resource in resources
|
for resource in resources
|
||||||
]
|
]
|
||||||
|
|
@ -347,6 +361,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
annotations: ToolAnnotations | None = None,
|
annotations: ToolAnnotations | None = None,
|
||||||
|
icons: list[Icon] | None = None,
|
||||||
structured_output: bool | None = None,
|
structured_output: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a tool to the server.
|
"""Add a tool to the server.
|
||||||
|
|
@ -371,6 +386,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
annotations=annotations,
|
annotations=annotations,
|
||||||
|
icons=icons,
|
||||||
structured_output=structured_output,
|
structured_output=structured_output,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -380,6 +396,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
annotations: ToolAnnotations | None = None,
|
annotations: ToolAnnotations | None = None,
|
||||||
|
icons: list[Icon] | None = None,
|
||||||
structured_output: bool | None = None,
|
structured_output: bool | None = None,
|
||||||
) -> Callable[[AnyFunction], AnyFunction]:
|
) -> Callable[[AnyFunction], AnyFunction]:
|
||||||
"""Decorator to register a tool.
|
"""Decorator to register a tool.
|
||||||
|
|
@ -426,6 +443,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
annotations=annotations,
|
annotations=annotations,
|
||||||
|
icons=icons,
|
||||||
structured_output=structured_output,
|
structured_output=structured_output,
|
||||||
)
|
)
|
||||||
return fn
|
return fn
|
||||||
|
|
@ -466,6 +484,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
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,
|
||||||
) -> Callable[[AnyFunction], AnyFunction]:
|
) -> Callable[[AnyFunction], AnyFunction]:
|
||||||
"""Decorator to register a function as a resource.
|
"""Decorator to register a function as a resource.
|
||||||
|
|
||||||
|
|
@ -540,6 +559,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
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Register as regular resource
|
# Register as regular resource
|
||||||
|
|
@ -550,6 +570,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
|
icons=icons,
|
||||||
)
|
)
|
||||||
self.add_resource(resource)
|
self.add_resource(resource)
|
||||||
return fn
|
return fn
|
||||||
|
|
@ -565,7 +586,11 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
self._prompt_manager.add_prompt(prompt)
|
self._prompt_manager.add_prompt(prompt)
|
||||||
|
|
||||||
def 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]:
|
) -> Callable[[AnyFunction], AnyFunction]:
|
||||||
"""Decorator to register a prompt.
|
"""Decorator to register a prompt.
|
||||||
|
|
||||||
|
|
@ -609,7 +634,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
)
|
)
|
||||||
|
|
||||||
def decorator(func: AnyFunction) -> AnyFunction:
|
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)
|
self.add_prompt(prompt)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
@ -980,6 +1005,7 @@ class FastMCP(Generic[LifespanResultT]):
|
||||||
)
|
)
|
||||||
for arg in (prompt.arguments or [])
|
for arg in (prompt.arguments or [])
|
||||||
],
|
],
|
||||||
|
icons=prompt.icons,
|
||||||
)
|
)
|
||||||
for prompt in prompts
|
for prompt in prompts
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from pydantic import BaseModel, Field
|
||||||
from mcp.server.fastmcp.exceptions import ToolError
|
from mcp.server.fastmcp.exceptions import ToolError
|
||||||
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter
|
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter
|
||||||
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
|
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:
|
if TYPE_CHECKING:
|
||||||
from mcp.server.fastmcp.server import Context
|
from mcp.server.fastmcp.server import Context
|
||||||
|
|
@ -33,6 +33,7 @@ class Tool(BaseModel):
|
||||||
is_async: bool = Field(description="Whether the tool is async")
|
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")
|
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")
|
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
|
@cached_property
|
||||||
def output_schema(self) -> dict[str, Any] | None:
|
def output_schema(self) -> dict[str, Any] | None:
|
||||||
|
|
@ -47,6 +48,7 @@ class Tool(BaseModel):
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
context_kwarg: str | None = None,
|
context_kwarg: str | None = None,
|
||||||
annotations: ToolAnnotations | None = None,
|
annotations: ToolAnnotations | None = None,
|
||||||
|
icons: list[Icon] | None = None,
|
||||||
structured_output: bool | None = None,
|
structured_output: bool | None = None,
|
||||||
) -> Tool:
|
) -> Tool:
|
||||||
"""Create a Tool from a function."""
|
"""Create a Tool from a function."""
|
||||||
|
|
@ -78,6 +80,7 @@ class Tool(BaseModel):
|
||||||
is_async=is_async,
|
is_async=is_async,
|
||||||
context_kwarg=context_kwarg,
|
context_kwarg=context_kwarg,
|
||||||
annotations=annotations,
|
annotations=annotations,
|
||||||
|
icons=icons,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run(
|
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.tools.base import Tool
|
||||||
from mcp.server.fastmcp.utilities.logging import get_logger
|
from mcp.server.fastmcp.utilities.logging import get_logger
|
||||||
from mcp.shared.context import LifespanContextT, RequestT
|
from mcp.shared.context import LifespanContextT, RequestT
|
||||||
from mcp.types import ToolAnnotations
|
from mcp.types import Icon, ToolAnnotations
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mcp.server.fastmcp.server import Context
|
from mcp.server.fastmcp.server import Context
|
||||||
|
|
@ -49,6 +49,7 @@ class ToolManager:
|
||||||
title: str | None = None,
|
title: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
annotations: ToolAnnotations | None = None,
|
annotations: ToolAnnotations | None = None,
|
||||||
|
icons: list[Icon] | None = None,
|
||||||
structured_output: bool | None = None,
|
structured_output: bool | None = None,
|
||||||
) -> Tool:
|
) -> Tool:
|
||||||
"""Add a tool to the server."""
|
"""Add a tool to the server."""
|
||||||
|
|
@ -58,6 +59,7 @@ class ToolManager:
|
||||||
title=title,
|
title=title,
|
||||||
description=description,
|
description=description,
|
||||||
annotations=annotations,
|
annotations=annotations,
|
||||||
|
icons=icons,
|
||||||
structured_output=structured_output,
|
structured_output=structured_output,
|
||||||
)
|
)
|
||||||
existing = self._tools.get(tool.name)
|
existing = self._tools.get(tool.name)
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
name: str,
|
name: str,
|
||||||
version: str | None = None,
|
version: str | None = None,
|
||||||
instructions: str | None = None,
|
instructions: str | None = None,
|
||||||
|
website_url: str | None = None,
|
||||||
|
icons: list[types.Icon] | None = None,
|
||||||
lifespan: Callable[
|
lifespan: Callable[
|
||||||
[Server[LifespanResultT, RequestT]],
|
[Server[LifespanResultT, RequestT]],
|
||||||
AbstractAsyncContextManager[LifespanResultT],
|
AbstractAsyncContextManager[LifespanResultT],
|
||||||
|
|
@ -144,6 +146,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.version = version
|
self.version = version
|
||||||
self.instructions = instructions
|
self.instructions = instructions
|
||||||
|
self.website_url = website_url
|
||||||
|
self.icons = icons
|
||||||
self.lifespan = lifespan
|
self.lifespan = lifespan
|
||||||
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
|
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
|
||||||
types.PingRequest: _ping_handler,
|
types.PingRequest: _ping_handler,
|
||||||
|
|
@ -177,6 +181,8 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
experimental_capabilities or {},
|
experimental_capabilities or {},
|
||||||
),
|
),
|
||||||
instructions=self.instructions,
|
instructions=self.instructions,
|
||||||
|
website_url=self.website_url,
|
||||||
|
icons=self.icons,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_capabilities(
|
def get_capabilities(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ and tools.
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from mcp.types import (
|
from mcp.types import (
|
||||||
|
Icon,
|
||||||
ServerCapabilities,
|
ServerCapabilities,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -15,3 +16,5 @@ class InitializationOptions(BaseModel):
|
||||||
server_version: str
|
server_version: str
|
||||||
capabilities: ServerCapabilities
|
capabilities: ServerCapabilities
|
||||||
instructions: str | None = None
|
instructions: str | None = None
|
||||||
|
website_url: str | None = None
|
||||||
|
icons: list[Icon] | None = None
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,8 @@ class ServerSession(
|
||||||
serverInfo=types.Implementation(
|
serverInfo=types.Implementation(
|
||||||
name=self._init_options.server_name,
|
name=self._init_options.server_name,
|
||||||
version=self._init_options.server_version,
|
version=self._init_options.server_version,
|
||||||
|
websiteUrl=self._init_options.website_url,
|
||||||
|
icons=self._init_options.icons,
|
||||||
),
|
),
|
||||||
instructions=self._init_options.instructions,
|
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):
|
class Implementation(BaseMetadata):
|
||||||
"""Describes the name and version of an MCP implementation."""
|
"""Describes the name and version of an MCP implementation."""
|
||||||
|
|
||||||
version: str
|
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")
|
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.
|
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
|
annotations: Annotations | None = None
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=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."""
|
"""An optional description of what this prompt provides."""
|
||||||
arguments: list[PromptArgument] | None = None
|
arguments: list[PromptArgument] | None = None
|
||||||
"""A list of arguments to use for templating the prompt."""
|
"""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)
|
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)
|
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
|
An optional JSON Schema object defining the structure of the tool's output
|
||||||
returned in the structuredContent field of a CallToolResult.
|
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
|
annotations: ToolAnnotations | None = None
|
||||||
"""Optional additional tool information."""
|
"""Optional additional tool information."""
|
||||||
meta: dict[str, Any] | None = Field(alias="_meta", default=None)
|
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