mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Implement icon support (#2121)
* Implement icon support in fastmcp * Fix icon feature tests - Update snapshot for ResourceTemplate to include icons field - Remove OAuth mounting tests (belong to PR #2119, not this feature) * Update docs
This commit is contained in:
parent
330eaed11f
commit
6f627b58fd
17 changed files with 944 additions and 8 deletions
|
|
@ -104,6 +104,7 @@
|
|||
"group": "Advanced Features",
|
||||
"icon": "stars",
|
||||
"pages": [
|
||||
"servers/icons",
|
||||
"servers/context",
|
||||
"servers/proxy",
|
||||
"servers/composition",
|
||||
|
|
|
|||
128
docs/servers/icons.mdx
Normal file
128
docs/servers/icons.mdx
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
---
|
||||
title: Icons
|
||||
description: Add visual icons to your servers, tools, resources, and prompts
|
||||
icon: image
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
Icons provide visual representations for your MCP servers and components, helping client applications present better user interfaces. When displayed in MCP clients, icons help users quickly identify and navigate your server's capabilities.
|
||||
|
||||
## Icon Format
|
||||
|
||||
Icons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies:
|
||||
|
||||
- **src**: URL or data URI pointing to the icon image
|
||||
- **mimeType** (optional): MIME type of the image (e.g., "image/png", "image/svg+xml")
|
||||
- **sizes** (optional): Array of size descriptors (e.g., ["48x48"], ["any"])
|
||||
|
||||
```python
|
||||
from mcp.types import Icon
|
||||
|
||||
icon = Icon(
|
||||
src="https://example.com/icon.png",
|
||||
mimeType="image/png",
|
||||
sizes=["48x48"]
|
||||
)
|
||||
```
|
||||
|
||||
## Server Icons
|
||||
|
||||
Add icons and a website URL to your server for display in client applications:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP(
|
||||
name="WeatherService",
|
||||
website_url="https://weather.example.com",
|
||||
icons=[
|
||||
Icon(
|
||||
src="https://weather.example.com/icon-48.png",
|
||||
mimeType="image/png",
|
||||
sizes=["48x48"]
|
||||
),
|
||||
Icon(
|
||||
src="https://weather.example.com/icon-96.png",
|
||||
mimeType="image/png",
|
||||
sizes=["96x96"]
|
||||
),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Server icons appear in MCP client interfaces to help users identify your server among others they may have installed.
|
||||
|
||||
## Component Icons
|
||||
|
||||
Icons can be added to individual tools, resources, resource templates, and prompts:
|
||||
|
||||
### Tool Icons
|
||||
|
||||
```python
|
||||
from mcp.types import Icon
|
||||
|
||||
@mcp.tool(
|
||||
icons=[Icon(src="https://example.com/calculator-icon.png")]
|
||||
)
|
||||
def calculate_sum(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
```
|
||||
|
||||
### Resource Icons
|
||||
|
||||
```python
|
||||
@mcp.resource(
|
||||
"config://settings",
|
||||
icons=[Icon(src="https://example.com/config-icon.png")]
|
||||
)
|
||||
def get_settings() -> dict:
|
||||
"""Retrieve application settings."""
|
||||
return {"theme": "dark", "language": "en"}
|
||||
```
|
||||
|
||||
### Resource Template Icons
|
||||
|
||||
```python
|
||||
@mcp.resource(
|
||||
"user://{user_id}/profile",
|
||||
icons=[Icon(src="https://example.com/user-icon.png")]
|
||||
)
|
||||
def get_user_profile(user_id: str) -> dict:
|
||||
"""Get a user's profile."""
|
||||
return {"id": user_id, "name": f"User {user_id}"}
|
||||
```
|
||||
|
||||
### Prompt Icons
|
||||
|
||||
```python
|
||||
@mcp.prompt(
|
||||
icons=[Icon(src="https://example.com/prompt-icon.png")]
|
||||
)
|
||||
def analyze_code(code: str):
|
||||
"""Create a prompt for code analysis."""
|
||||
return f"Please analyze this code:\n\n{code}"
|
||||
```
|
||||
|
||||
## Using Data URIs
|
||||
|
||||
For small icons or when you want to embed the icon directly, use data URIs:
|
||||
|
||||
```python
|
||||
from mcp.types import Icon
|
||||
|
||||
# SVG icon as data URI
|
||||
svg_icon = Icon(
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+",
|
||||
mimeType="image/svg+xml"
|
||||
)
|
||||
|
||||
@mcp.tool(icons=[svg_icon])
|
||||
def my_tool() -> str:
|
||||
"""A tool with an embedded SVG icon."""
|
||||
return "result"
|
||||
```
|
||||
|
|
@ -97,9 +97,15 @@ def data_analysis_prompt(
|
|||
A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="icons" type="list[Icon] | None">
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
Optional list of icon representations for this prompt. See [Icons](/servers/icons) for detailed examples
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="meta" type="dict[str, Any] | None">
|
||||
<VersionBadge version="2.11.0" />
|
||||
|
||||
|
||||
Optional meta information about the prompt. This data is passed through to the MCP client as the `_meta` field of the client-side prompt object and can be used for custom metadata, versioning, or other application-specific purposes.
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,12 @@ def get_application_status() -> dict:
|
|||
A boolean to enable or disable the resource. See [Disabling Resources](#disabling-resources) for more information
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="icons" type="list[Icon] | None">
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
Optional list of icon representations for this resource or template. See [Icons](/servers/icons) for detailed examples
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="annotations" type="Annotations | dict | None">
|
||||
An optional `Annotations` object or dictionary to add additional metadata about the resource.
|
||||
<Expandable title="Annotations attributes">
|
||||
|
|
|
|||
|
|
@ -40,6 +40,22 @@ The `FastMCP` constructor accepts several arguments:
|
|||
Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="version" type="str | None">
|
||||
Version string for your server. If not provided, defaults to the FastMCP library version
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="website_url" type="str | None">
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
URL to a website with more information about your server. Displayed in client applications
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="icons" type="list[Icon] | None">
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
List of icon representations for your server. Icons help users visually identify your server in client applications. See [Icons](/servers/icons) for detailed examples
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="auth" type="OAuthProvider | TokenVerifier | None">
|
||||
Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration options
|
||||
</ParamField>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ def search_products_implementation(query: str, category: str | None = None) -> l
|
|||
A boolean to enable or disable the tool. See [Disabling Tools](#disabling-tools) for more information
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="icons" type="list[Icon] | None">
|
||||
<VersionBadge version="2.14.0" />
|
||||
|
||||
Optional list of icon representations for this tool. See [Icons](/servers/icons) for detailed examples
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="exclude_args" type="list[str] | None">
|
||||
A list of argument names to exclude from the tool schema shown to the LLM. See [Excluding Arguments](#excluding-arguments) for more information
|
||||
</ParamField>
|
||||
|
|
|
|||
|
|
@ -713,6 +713,10 @@ async def inspect(
|
|||
console.print(f" Name: {info.name}")
|
||||
if info.version:
|
||||
console.print(f" Version: {info.version}")
|
||||
if info.website_url:
|
||||
console.print(f" Website: {info.website_url}")
|
||||
if info.icons:
|
||||
console.print(f" Icons: {len(info.icons)}")
|
||||
console.print(f" Generation: {info.server_generation}")
|
||||
if info.instructions:
|
||||
console.print(f" Instructions: {info.instructions}")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Sequence
|
|||
from typing import Any
|
||||
|
||||
import pydantic_core
|
||||
from mcp.types import ContentBlock, PromptMessage, Role, TextContent
|
||||
from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
|
||||
from mcp.types import Prompt as MCPPrompt
|
||||
from mcp.types import PromptArgument as MCPPromptArgument
|
||||
from pydantic import Field, TypeAdapter
|
||||
|
|
@ -105,6 +105,7 @@ class Prompt(FastMCPComponent):
|
|||
description=overrides.get("description", self.description),
|
||||
arguments=arguments,
|
||||
title=overrides.get("title", self.title),
|
||||
icons=overrides.get("icons", self.icons),
|
||||
_meta=overrides.get(
|
||||
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
||||
),
|
||||
|
|
@ -116,6 +117,7 @@ class Prompt(FastMCPComponent):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
|
|
@ -133,6 +135,7 @@ class Prompt(FastMCPComponent):
|
|||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
meta=meta,
|
||||
|
|
@ -162,6 +165,7 @@ class FunctionPrompt(Prompt):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
|
|
@ -255,6 +259,7 @@ class FunctionPrompt(Prompt):
|
|||
name=func_name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
arguments=arguments,
|
||||
tags=tags or set(),
|
||||
enabled=enabled if enabled is not None else True,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from collections.abc import Callable
|
|||
from typing import TYPE_CHECKING, Annotated, Any
|
||||
|
||||
import pydantic_core
|
||||
from mcp.types import Annotations
|
||||
from mcp.types import Annotations, Icon
|
||||
from mcp.types import Resource as MCPResource
|
||||
from pydantic import (
|
||||
AnyUrl,
|
||||
|
|
@ -72,6 +72,7 @@ class Resource(FastMCPComponent):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
mime_type: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
|
|
@ -84,6 +85,7 @@ class Resource(FastMCPComponent):
|
|||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
|
|
@ -132,6 +134,7 @@ class Resource(FastMCPComponent):
|
|||
description=overrides.get("description", self.description),
|
||||
mimeType=overrides.get("mimeType", self.mime_type),
|
||||
title=overrides.get("title", self.title),
|
||||
icons=overrides.get("icons", self.icons),
|
||||
annotations=overrides.get("annotations", self.annotations),
|
||||
_meta=overrides.get(
|
||||
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
||||
|
|
@ -175,6 +178,7 @@ class FunctionResource(Resource):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
mime_type: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
|
|
@ -190,6 +194,7 @@ class FunctionResource(Resource):
|
|||
name=name or get_fn_name(fn),
|
||||
title=title,
|
||||
description=description or inspect.getdoc(fn),
|
||||
icons=icons,
|
||||
mime_type=mime_type or "text/plain",
|
||||
tags=tags or set(),
|
||||
enabled=enabled if enabled is not None else True,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from collections.abc import Callable
|
|||
from typing import Any
|
||||
from urllib.parse import parse_qs, unquote
|
||||
|
||||
from mcp.types import Annotations
|
||||
from mcp.types import Annotations, Icon
|
||||
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
||||
from pydantic import (
|
||||
Field,
|
||||
|
|
@ -133,6 +133,7 @@ class ResourceTemplate(FastMCPComponent):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
mime_type: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
|
|
@ -145,6 +146,7 @@ class ResourceTemplate(FastMCPComponent):
|
|||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
|
|
@ -202,6 +204,7 @@ class ResourceTemplate(FastMCPComponent):
|
|||
description=overrides.get("description", self.description),
|
||||
mimeType=overrides.get("mimeType", self.mime_type),
|
||||
title=overrides.get("title", self.title),
|
||||
icons=overrides.get("icons", self.icons),
|
||||
annotations=overrides.get("annotations", self.annotations),
|
||||
_meta=overrides.get(
|
||||
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
||||
|
|
@ -285,6 +288,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
mime_type: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
|
|
@ -388,6 +392,7 @@ class FunctionResourceTemplate(ResourceTemplate):
|
|||
name=func_name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type or "text/plain",
|
||||
fn=fn,
|
||||
parameters=parameters,
|
||||
|
|
|
|||
|
|
@ -138,6 +138,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
instructions: str | None = None,
|
||||
*,
|
||||
version: str | None = None,
|
||||
website_url: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
auth: AuthProvider | None | NotSetT = NotSet,
|
||||
middleware: list[Middleware] | None = None,
|
||||
lifespan: LifespanCallable | None = None,
|
||||
|
|
@ -202,6 +204,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=name or self.generate_name(),
|
||||
version=version or fastmcp.__version__,
|
||||
instructions=instructions,
|
||||
website_url=website_url,
|
||||
icons=icons,
|
||||
lifespan=_lifespan_proxy(fastmcp_server=self),
|
||||
)
|
||||
|
||||
|
|
@ -1331,6 +1335,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
|
|
@ -1347,6 +1352,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
|
|
@ -1362,6 +1368,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
output_schema: dict[str, Any] | None | NotSetT = NotSet,
|
||||
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
||||
|
|
@ -1445,6 +1452,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=tool_name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=cast(ToolAnnotations | None, annotations),
|
||||
|
|
@ -1478,6 +1486,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=tool_name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
output_schema=output_schema,
|
||||
annotations=annotations,
|
||||
|
|
@ -1575,6 +1584,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
mime_type: str | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
|
|
@ -1674,6 +1684,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
|
|
@ -1689,6 +1700,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
mime_type=mime_type,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
|
|
@ -1735,6 +1747,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
|
|
@ -1748,6 +1761,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
|
|
@ -1760,6 +1774,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[mcp.types.Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
enabled: bool | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
|
|
@ -1859,6 +1874,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=prompt_name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
meta=meta,
|
||||
|
|
@ -1889,6 +1905,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
name=prompt_name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
enabled=enabled,
|
||||
meta=meta,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from typing import (
|
|||
|
||||
import mcp.types
|
||||
import pydantic_core
|
||||
from mcp.types import ContentBlock, TextContent, ToolAnnotations
|
||||
from mcp.types import ContentBlock, Icon, TextContent, ToolAnnotations
|
||||
from mcp.types import Tool as MCPTool
|
||||
from pydantic import Field, PydanticSchemaGenerationError
|
||||
from typing_extensions import TypeVar
|
||||
|
|
@ -156,6 +156,7 @@ class Tool(FastMCPComponent):
|
|||
description=overrides.get("description", self.description),
|
||||
inputSchema=overrides.get("inputSchema", self.parameters),
|
||||
outputSchema=overrides.get("outputSchema", self.output_schema),
|
||||
icons=overrides.get("icons", self.icons),
|
||||
annotations=overrides.get("annotations", self.annotations),
|
||||
_meta=overrides.get(
|
||||
"_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
|
||||
|
|
@ -168,6 +169,7 @@ class Tool(FastMCPComponent):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
|
|
@ -182,6 +184,7 @@ class Tool(FastMCPComponent):
|
|||
name=name,
|
||||
title=title,
|
||||
description=description,
|
||||
icons=icons,
|
||||
tags=tags,
|
||||
annotations=annotations,
|
||||
exclude_args=exclude_args,
|
||||
|
|
@ -248,6 +251,7 @@ class FunctionTool(Tool):
|
|||
name: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
icons: list[Icon] | None = None,
|
||||
tags: set[str] | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
exclude_args: list[str] | None = None,
|
||||
|
|
@ -291,6 +295,7 @@ class FunctionTool(Tool):
|
|||
name=name or parsed_fn.name,
|
||||
title=title,
|
||||
description=description or parsed_fn.description,
|
||||
icons=icons,
|
||||
parameters=parsed_fn.input_schema,
|
||||
output_schema=final_output_schema,
|
||||
annotations=annotations,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
from collections.abc import Sequence
|
||||
from typing import Annotated, Any, TypedDict
|
||||
|
||||
from mcp.types import Icon
|
||||
from pydantic import BeforeValidator, Field, PrivateAttr
|
||||
from typing_extensions import Self, TypeVar
|
||||
|
||||
|
|
@ -39,6 +40,10 @@ class FastMCPComponent(FastMCPBaseModel):
|
|||
default=None,
|
||||
description="The description of the component.",
|
||||
)
|
||||
icons: list[Icon] | None = Field(
|
||||
default=None,
|
||||
description="Optional list of icons for this component to display in user interfaces.",
|
||||
)
|
||||
tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(
|
||||
default_factory=set,
|
||||
description="Tags for the component.",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class ToolInfo:
|
|||
tags: list[str] | None = None
|
||||
enabled: bool | None = None
|
||||
title: str | None = None
|
||||
icons: list[dict[str, Any]] | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ class PromptInfo:
|
|||
tags: list[str] | None = None
|
||||
enabled: bool | None = None
|
||||
title: str | None = None
|
||||
icons: list[dict[str, Any]] | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
|
@ -58,6 +60,7 @@ class ResourceInfo:
|
|||
tags: list[str] | None = None
|
||||
enabled: bool | None = None
|
||||
title: str | None = None
|
||||
icons: list[dict[str, Any]] | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
|
@ -75,6 +78,7 @@ class TemplateInfo:
|
|||
tags: list[str] | None = None
|
||||
enabled: bool | None = None
|
||||
title: str | None = None
|
||||
icons: list[dict[str, Any]] | None = None
|
||||
meta: dict[str, Any] | None = None
|
||||
|
||||
|
||||
|
|
@ -85,6 +89,8 @@ class FastMCPInfo:
|
|||
name: str
|
||||
instructions: str | None
|
||||
version: str | None # The server's own version string (if specified)
|
||||
website_url: str | None
|
||||
icons: list[dict[str, Any]] | None
|
||||
fastmcp_version: str # Version of FastMCP generating this manifest
|
||||
mcp_version: str # Version of MCP protocol library
|
||||
server_generation: int # Server generation: 1 (mcp package) or 2 (fastmcp)
|
||||
|
|
@ -125,6 +131,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|||
tags=list(tool.tags) if tool.tags else None,
|
||||
enabled=tool.enabled,
|
||||
title=tool.title,
|
||||
icons=[icon.model_dump() for icon in tool.icons]
|
||||
if tool.icons
|
||||
else None,
|
||||
meta=tool.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -143,6 +152,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|||
tags=list(prompt.tags) if prompt.tags else None,
|
||||
enabled=prompt.enabled,
|
||||
title=prompt.title,
|
||||
icons=[icon.model_dump() for icon in prompt.icons]
|
||||
if prompt.icons
|
||||
else None,
|
||||
meta=prompt.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -163,6 +175,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|||
tags=list(resource.tags) if resource.tags else None,
|
||||
enabled=resource.enabled,
|
||||
title=resource.title,
|
||||
icons=[icon.model_dump() for icon in resource.icons]
|
||||
if resource.icons
|
||||
else None,
|
||||
meta=resource.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -184,6 +199,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|||
tags=list(template.tags) if template.tags else None,
|
||||
enabled=template.enabled,
|
||||
title=template.title,
|
||||
icons=[icon.model_dump() for icon in template.icons]
|
||||
if template.icons
|
||||
else None,
|
||||
meta=template.meta,
|
||||
)
|
||||
)
|
||||
|
|
@ -196,13 +214,25 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|||
"logging": {},
|
||||
}
|
||||
|
||||
# Extract server-level icons and website_url
|
||||
server_icons = (
|
||||
[icon.model_dump() for icon in mcp._mcp_server.icons]
|
||||
if hasattr(mcp._mcp_server, "icons") and mcp._mcp_server.icons
|
||||
else None
|
||||
)
|
||||
server_website_url = (
|
||||
mcp._mcp_server.website_url if hasattr(mcp._mcp_server, "website_url") else None
|
||||
)
|
||||
|
||||
return FastMCPInfo(
|
||||
name=mcp.name,
|
||||
instructions=mcp.instructions,
|
||||
version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
|
||||
website_url=server_website_url,
|
||||
icons=server_icons,
|
||||
fastmcp_version=fastmcp.__version__,
|
||||
mcp_version=importlib.metadata.version("mcp"),
|
||||
server_generation=2, # FastMCP v2
|
||||
version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
|
||||
tools=tool_infos,
|
||||
prompts=prompt_infos,
|
||||
resources=resource_infos,
|
||||
|
|
@ -247,6 +277,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
tags=None, # v1 doesn't have tags
|
||||
enabled=None, # v1 doesn't have enabled field
|
||||
title=None, # v1 doesn't have title
|
||||
icons=[icon.model_dump() for icon in mcp_tool.icons]
|
||||
if hasattr(mcp_tool, "icons") and mcp_tool.icons
|
||||
else None,
|
||||
meta=None, # v1 doesn't have meta field
|
||||
)
|
||||
)
|
||||
|
|
@ -268,6 +301,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
tags=None, # v1 doesn't have tags
|
||||
enabled=None, # v1 doesn't have enabled field
|
||||
title=None, # v1 doesn't have title
|
||||
icons=[icon.model_dump() for icon in mcp_prompt.icons]
|
||||
if hasattr(mcp_prompt, "icons") and mcp_prompt.icons
|
||||
else None,
|
||||
meta=None, # v1 doesn't have meta field
|
||||
)
|
||||
)
|
||||
|
|
@ -286,6 +322,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
tags=None, # v1 doesn't have tags
|
||||
enabled=None, # v1 doesn't have enabled field
|
||||
title=None, # v1 doesn't have title
|
||||
icons=[icon.model_dump() for icon in mcp_resource.icons]
|
||||
if hasattr(mcp_resource, "icons") and mcp_resource.icons
|
||||
else None,
|
||||
meta=None, # v1 doesn't have meta field
|
||||
)
|
||||
)
|
||||
|
|
@ -305,6 +344,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
tags=None, # v1 doesn't have tags
|
||||
enabled=None, # v1 doesn't have enabled field
|
||||
title=None, # v1 doesn't have title
|
||||
icons=[icon.model_dump() for icon in mcp_template.icons]
|
||||
if hasattr(mcp_template, "icons") and mcp_template.icons
|
||||
else None,
|
||||
meta=None, # v1 doesn't have meta field
|
||||
)
|
||||
)
|
||||
|
|
@ -317,13 +359,26 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|||
"logging": {},
|
||||
}
|
||||
|
||||
# Extract server-level icons and website_url from serverInfo
|
||||
server_info = client.initialize_result.serverInfo
|
||||
server_icons = (
|
||||
[icon.model_dump() for icon in server_info.icons]
|
||||
if hasattr(server_info, "icons") and server_info.icons
|
||||
else None
|
||||
)
|
||||
server_website_url = (
|
||||
server_info.websiteUrl if hasattr(server_info, "websiteUrl") else None
|
||||
)
|
||||
|
||||
return FastMCPInfo(
|
||||
name=mcp._mcp_server.name,
|
||||
instructions=mcp._mcp_server.instructions,
|
||||
version=mcp._mcp_server.version,
|
||||
website_url=server_website_url,
|
||||
icons=server_icons,
|
||||
fastmcp_version=fastmcp.__version__, # Version generating this manifest
|
||||
mcp_version=importlib.metadata.version("mcp"),
|
||||
server_generation=1, # MCP v1
|
||||
version=mcp._mcp_server.version,
|
||||
tools=tool_infos,
|
||||
prompts=prompt_infos,
|
||||
resources=resource_infos,
|
||||
|
|
@ -368,6 +423,8 @@ async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
|
|||
"name": info.name,
|
||||
"instructions": info.instructions,
|
||||
"version": info.version,
|
||||
"website_url": info.website_url,
|
||||
"icons": info.icons,
|
||||
"generation": info.server_generation,
|
||||
"capabilities": info.capabilities,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ class TestLoggingMiddleware:
|
|||
|
||||
assert get_log_lines(caplog) == snapshot(
|
||||
[
|
||||
'{"event": "request_start", "method": "test_method", "source": "client", "payload": "{\\"name\\":\\"tmpl\\",\\"title\\":null,\\"description\\":null,\\"tags\\":[],\\"meta\\":null,\\"enabled\\":true,\\"uri_template\\":\\"tmpl://{id}\\",\\"mime_type\\":\\"text/plain\\",\\"parameters\\":{\\"id\\":{\\"type\\":\\"string\\"}},\\"annotations\\":null}", "payload_type": "ResourceTemplate"}',
|
||||
'{"event": "request_start", "method": "test_method", "source": "client", "payload": "{\\"name\\":\\"tmpl\\",\\"title\\":null,\\"description\\":null,\\"icons\\":null,\\"tags\\":[],\\"meta\\":null,\\"enabled\\":true,\\"uri_template\\":\\"tmpl://{id}\\",\\"mime_type\\":\\"text/plain\\",\\"parameters\\":{\\"id\\":{\\"type\\":\\"string\\"}},\\"annotations\\":null}", "payload_type": "ResourceTemplate"}',
|
||||
'{"event": "request_success", "method": "test_method", "source": "client", "duration_ms": 0.02}',
|
||||
]
|
||||
)
|
||||
|
|
|
|||
342
tests/server/test_icons.py
Normal file
342
tests/server/test_icons.py
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
"""Tests for icon support across all MCP object types."""
|
||||
|
||||
from mcp.types import Icon
|
||||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.prompts import Message, Prompt
|
||||
from fastmcp.resources import Resource
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.tools import Tool
|
||||
|
||||
|
||||
class TestServerIcons:
|
||||
"""Test icon support at the server/implementation level."""
|
||||
|
||||
async def test_server_with_icons_and_website_url(self):
|
||||
"""Test that server accepts icons and websiteUrl in constructor."""
|
||||
icons = [
|
||||
Icon(
|
||||
src="https://example.com/icon.png",
|
||||
mimeType="image/png",
|
||||
sizes=["48x48"],
|
||||
),
|
||||
Icon(
|
||||
src="data:image/svg+xml;base64,PHN2Zz48L3N2Zz4=",
|
||||
mimeType="image/svg+xml",
|
||||
sizes=["any"],
|
||||
),
|
||||
]
|
||||
|
||||
mcp = FastMCP(
|
||||
name="TestServer",
|
||||
version="1.0.0",
|
||||
website_url="https://example.com",
|
||||
icons=icons,
|
||||
)
|
||||
|
||||
# Verify that icons and website_url are passed to the underlying server
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
assert server_info.websiteUrl == "https://example.com"
|
||||
assert server_info.icons == icons
|
||||
|
||||
async def test_server_without_icons_and_website_url(self):
|
||||
"""Test that server works without icons and websiteUrl."""
|
||||
mcp = FastMCP(name="TestServer")
|
||||
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
assert server_info.websiteUrl is None
|
||||
assert server_info.icons is None
|
||||
|
||||
|
||||
class TestToolIcons:
|
||||
"""Test icon support for tools."""
|
||||
|
||||
async def test_tool_with_icons(self):
|
||||
"""Test that tools can have icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
icons = [
|
||||
Icon(src="https://example.com/tool-icon.png", mimeType="image/png"),
|
||||
]
|
||||
|
||||
@mcp.tool(icons=icons)
|
||||
def my_tool(name: str) -> str:
|
||||
"""A tool with an icon."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
assert tool.icons == icons
|
||||
|
||||
async def test_tool_from_function_with_icons(self):
|
||||
"""Test creating a tool from a function with icons."""
|
||||
icons = [Icon(src="https://example.com/icon.png")]
|
||||
|
||||
def my_function(x: int) -> int:
|
||||
"""A function."""
|
||||
return x * 2
|
||||
|
||||
tool = Tool.from_function(my_function, icons=icons)
|
||||
assert tool.icons == icons
|
||||
|
||||
# Verify it converts to MCP tool correctly
|
||||
mcp_tool = tool.to_mcp_tool()
|
||||
assert mcp_tool.icons == icons
|
||||
|
||||
async def test_tool_without_icons(self):
|
||||
"""Test that tools work without icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
@mcp.tool
|
||||
def my_tool(name: str) -> str:
|
||||
"""A tool without an icon."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
assert len(tools) == 1
|
||||
tool = tools[0]
|
||||
assert tool.icons is None
|
||||
|
||||
|
||||
class TestResourceIcons:
|
||||
"""Test icon support for resources."""
|
||||
|
||||
async def test_resource_with_icons(self):
|
||||
"""Test that resources can have icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
icons = [Icon(src="https://example.com/resource-icon.png")]
|
||||
|
||||
@mcp.resource("test://resource", icons=icons)
|
||||
def my_resource() -> str:
|
||||
"""A resource with an icon."""
|
||||
return "Resource content"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
resources = await client.list_resources()
|
||||
assert len(resources) == 1
|
||||
resource = resources[0]
|
||||
assert resource.icons == icons
|
||||
|
||||
async def test_resource_from_function_with_icons(self):
|
||||
"""Test creating a resource from a function with icons."""
|
||||
icons = [Icon(src="https://example.com/icon.png")]
|
||||
|
||||
def my_function() -> str:
|
||||
"""A function."""
|
||||
return "content"
|
||||
|
||||
resource = Resource.from_function(
|
||||
my_function,
|
||||
uri="test://resource",
|
||||
icons=icons,
|
||||
)
|
||||
assert resource.icons == icons
|
||||
|
||||
# Verify it converts to MCP resource correctly
|
||||
mcp_resource = resource.to_mcp_resource()
|
||||
assert mcp_resource.icons == icons
|
||||
|
||||
async def test_resource_without_icons(self):
|
||||
"""Test that resources work without icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
@mcp.resource("test://resource")
|
||||
def my_resource() -> str:
|
||||
"""A resource without an icon."""
|
||||
return "Resource content"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
resources = await client.list_resources()
|
||||
assert len(resources) == 1
|
||||
resource = resources[0]
|
||||
assert resource.icons is None
|
||||
|
||||
|
||||
class TestResourceTemplateIcons:
|
||||
"""Test icon support for resource templates."""
|
||||
|
||||
async def test_resource_template_with_icons(self):
|
||||
"""Test that resource templates can have icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
icons = [Icon(src="https://example.com/template-icon.png")]
|
||||
|
||||
@mcp.resource("test://resource/{id}", icons=icons)
|
||||
def my_template(id: str) -> str:
|
||||
"""A resource template with an icon."""
|
||||
return f"Resource {id}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
templates = await client.list_resource_templates()
|
||||
assert len(templates) == 1
|
||||
template = templates[0]
|
||||
assert template.icons == icons
|
||||
|
||||
async def test_resource_template_from_function_with_icons(self):
|
||||
"""Test creating a resource template from a function with icons."""
|
||||
icons = [Icon(src="https://example.com/icon.png")]
|
||||
|
||||
def my_function(id: str) -> str:
|
||||
"""A function."""
|
||||
return f"content-{id}"
|
||||
|
||||
template = ResourceTemplate.from_function(
|
||||
my_function,
|
||||
uri_template="test://resource/{id}",
|
||||
icons=icons,
|
||||
)
|
||||
assert template.icons == icons
|
||||
|
||||
# Verify it converts to MCP template correctly
|
||||
mcp_template = template.to_mcp_template()
|
||||
assert mcp_template.icons == icons
|
||||
|
||||
async def test_resource_template_without_icons(self):
|
||||
"""Test that resource templates work without icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
@mcp.resource("test://resource/{id}")
|
||||
def my_template(id: str) -> str:
|
||||
"""A resource template without an icon."""
|
||||
return f"Resource {id}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
templates = await client.list_resource_templates()
|
||||
assert len(templates) == 1
|
||||
template = templates[0]
|
||||
assert template.icons is None
|
||||
|
||||
|
||||
class TestPromptIcons:
|
||||
"""Test icon support for prompts."""
|
||||
|
||||
async def test_prompt_with_icons(self):
|
||||
"""Test that prompts can have icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
icons = [Icon(src="https://example.com/prompt-icon.png")]
|
||||
|
||||
@mcp.prompt(icons=icons)
|
||||
def my_prompt(name: str):
|
||||
"""A prompt with an icon."""
|
||||
return Message(f"Hello, {name}!")
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
assert len(prompts) == 1
|
||||
prompt = prompts[0]
|
||||
assert prompt.icons == icons
|
||||
|
||||
async def test_prompt_from_function_with_icons(self):
|
||||
"""Test creating a prompt from a function with icons."""
|
||||
icons = [Icon(src="https://example.com/icon.png")]
|
||||
|
||||
def my_function(topic: str):
|
||||
"""A function."""
|
||||
return Message(f"Tell me about {topic}")
|
||||
|
||||
prompt = Prompt.from_function(my_function, icons=icons)
|
||||
assert prompt.icons == icons
|
||||
|
||||
# Verify it converts to MCP prompt correctly
|
||||
mcp_prompt = prompt.to_mcp_prompt()
|
||||
assert mcp_prompt.icons == icons
|
||||
|
||||
async def test_prompt_without_icons(self):
|
||||
"""Test that prompts work without icons."""
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
@mcp.prompt
|
||||
def my_prompt(name: str):
|
||||
"""A prompt without an icon."""
|
||||
return Message(f"Hello, {name}!")
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
assert len(prompts) == 1
|
||||
prompt = prompts[0]
|
||||
assert prompt.icons is None
|
||||
|
||||
|
||||
class TestIconTypes:
|
||||
"""Test different types of icon data."""
|
||||
|
||||
async def test_multiple_icon_sizes(self):
|
||||
"""Test that multiple icon sizes can be specified."""
|
||||
icons = [
|
||||
Icon(
|
||||
src="https://example.com/icon-48.png",
|
||||
mimeType="image/png",
|
||||
sizes=["48x48"],
|
||||
),
|
||||
Icon(
|
||||
src="https://example.com/icon-96.png",
|
||||
mimeType="image/png",
|
||||
sizes=["96x96"],
|
||||
),
|
||||
Icon(
|
||||
src="https://example.com/icon.svg",
|
||||
mimeType="image/svg+xml",
|
||||
sizes=["any"],
|
||||
),
|
||||
]
|
||||
|
||||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
assert len(server_info.icons) == 3
|
||||
assert server_info.icons == icons
|
||||
|
||||
async def test_data_uri_icon(self):
|
||||
"""Test using data URIs for icons."""
|
||||
# Simple SVG data URI
|
||||
data_uri = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+"
|
||||
|
||||
icons = [Icon(src=data_uri, mimeType="image/svg+xml")]
|
||||
|
||||
mcp = FastMCP("TestServer")
|
||||
|
||||
@mcp.tool(icons=icons)
|
||||
def my_tool() -> str:
|
||||
"""A tool with a data URI icon."""
|
||||
return "result"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
tools = await client.list_tools()
|
||||
assert tools[0].icons[0].src == data_uri
|
||||
|
||||
async def test_icon_without_optional_fields(self):
|
||||
"""Test that icons work with only the src field."""
|
||||
icons = [Icon(src="https://example.com/icon.png")]
|
||||
|
||||
mcp = FastMCP("TestServer", icons=icons)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
server_info = client.initialize_result.serverInfo
|
||||
assert server_info.icons[0].src == "https://example.com/icon.png"
|
||||
assert server_info.icons[0].mimeType is None
|
||||
assert server_info.icons[0].sizes is None
|
||||
|
||||
|
||||
class TestIconImport:
|
||||
"""Test that Icon must be imported from mcp.types."""
|
||||
|
||||
def test_icon_import(self):
|
||||
"""Test that Icon must be imported from mcp.types, not fastmcp."""
|
||||
# Icon should NOT be available from fastmcp
|
||||
import fastmcp
|
||||
|
||||
assert not hasattr(fastmcp, "Icon")
|
||||
|
||||
# Icon should be imported from mcp.types
|
||||
from mcp.types import Icon as MCPIcon
|
||||
|
||||
icon = MCPIcon(src="https://example.com/icon.png")
|
||||
assert icon.src == "https://example.com/icon.png"
|
||||
|
|
@ -40,6 +40,8 @@ class TestFastMCPInfo:
|
|||
mcp_version="1.0.0",
|
||||
server_generation=2,
|
||||
version="1.0.0",
|
||||
website_url=None,
|
||||
icons=None,
|
||||
tools=[tool],
|
||||
prompts=[],
|
||||
resources=[],
|
||||
|
|
@ -66,6 +68,8 @@ class TestFastMCPInfo:
|
|||
mcp_version="1.0.0",
|
||||
server_generation=2,
|
||||
version="1.0.0",
|
||||
website_url=None,
|
||||
icons=None,
|
||||
tools=[],
|
||||
prompts=[],
|
||||
resources=[],
|
||||
|
|
@ -604,6 +608,330 @@ class TestFastMCP1xCompatibility:
|
|||
assert len(info2x.templates) == 0
|
||||
|
||||
|
||||
class TestIconExtraction:
|
||||
"""Tests for icon extraction in inspect."""
|
||||
|
||||
async def test_server_icons_and_website(self):
|
||||
"""Test that server-level icons and website_url are extracted."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP(
|
||||
"IconServer",
|
||||
website_url="https://example.com",
|
||||
icons=[
|
||||
Icon(
|
||||
src="https://example.com/icon.png",
|
||||
mimeType="image/png",
|
||||
sizes=["48x48"],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert info.website_url == "https://example.com"
|
||||
assert info.icons is not None
|
||||
assert len(info.icons) == 1
|
||||
assert info.icons[0]["src"] == "https://example.com/icon.png"
|
||||
assert info.icons[0]["mimeType"] == "image/png"
|
||||
assert info.icons[0]["sizes"] == ["48x48"]
|
||||
|
||||
async def test_server_without_icons(self):
|
||||
"""Test that servers without icons have None for icons and website_url."""
|
||||
mcp = FastMCP("NoIconServer")
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert info.website_url is None
|
||||
assert info.icons is None
|
||||
|
||||
async def test_tool_icons(self):
|
||||
"""Test that tool icons are extracted."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP("ToolIconServer")
|
||||
|
||||
@mcp.tool(
|
||||
icons=[
|
||||
Icon(
|
||||
src="https://example.com/calculator.png",
|
||||
mimeType="image/png",
|
||||
)
|
||||
]
|
||||
)
|
||||
def calculate(x: int) -> int:
|
||||
"""Calculate something."""
|
||||
return x * 2
|
||||
|
||||
@mcp.tool
|
||||
def no_icon_tool() -> str:
|
||||
"""Tool without icon."""
|
||||
return "no icon"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert len(info.tools) == 2
|
||||
|
||||
# Find the calculate tool
|
||||
calculate_tool = next(t for t in info.tools if t.name == "calculate")
|
||||
assert calculate_tool.icons is not None
|
||||
assert len(calculate_tool.icons) == 1
|
||||
assert calculate_tool.icons[0]["src"] == "https://example.com/calculator.png"
|
||||
|
||||
# Find the no_icon tool
|
||||
no_icon = next(t for t in info.tools if t.name == "no_icon_tool")
|
||||
assert no_icon.icons is None
|
||||
|
||||
async def test_resource_icons(self):
|
||||
"""Test that resource icons are extracted."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP("ResourceIconServer")
|
||||
|
||||
@mcp.resource(
|
||||
"resource://data",
|
||||
icons=[Icon(src="https://example.com/data.png", mimeType="image/png")],
|
||||
)
|
||||
def get_data() -> str:
|
||||
"""Get data."""
|
||||
return "data"
|
||||
|
||||
@mcp.resource("resource://no-icon")
|
||||
def get_no_icon() -> str:
|
||||
"""Get data without icon."""
|
||||
return "no icon"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert len(info.resources) == 2
|
||||
|
||||
# Find the data resource
|
||||
data_resource = next(r for r in info.resources if r.uri == "resource://data")
|
||||
assert data_resource.icons is not None
|
||||
assert len(data_resource.icons) == 1
|
||||
assert data_resource.icons[0]["src"] == "https://example.com/data.png"
|
||||
|
||||
# Find the no-icon resource
|
||||
no_icon = next(r for r in info.resources if r.uri == "resource://no-icon")
|
||||
assert no_icon.icons is None
|
||||
|
||||
async def test_template_icons(self):
|
||||
"""Test that resource template icons are extracted."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP("TemplateIconServer")
|
||||
|
||||
@mcp.resource(
|
||||
"resource://user/{id}",
|
||||
icons=[Icon(src="https://example.com/user.png", mimeType="image/png")],
|
||||
)
|
||||
def get_user(id: str) -> str:
|
||||
"""Get user by ID."""
|
||||
return f"user {id}"
|
||||
|
||||
@mcp.resource("resource://item/{id}")
|
||||
def get_item(id: str) -> str:
|
||||
"""Get item without icon."""
|
||||
return f"item {id}"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert len(info.templates) == 2
|
||||
|
||||
# Find the user template
|
||||
user_template = next(
|
||||
t for t in info.templates if t.uri_template == "resource://user/{id}"
|
||||
)
|
||||
assert user_template.icons is not None
|
||||
assert len(user_template.icons) == 1
|
||||
assert user_template.icons[0]["src"] == "https://example.com/user.png"
|
||||
|
||||
# Find the no-icon template
|
||||
no_icon = next(
|
||||
t for t in info.templates if t.uri_template == "resource://item/{id}"
|
||||
)
|
||||
assert no_icon.icons is None
|
||||
|
||||
async def test_prompt_icons(self):
|
||||
"""Test that prompt icons are extracted."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP("PromptIconServer")
|
||||
|
||||
@mcp.prompt(
|
||||
icons=[Icon(src="https://example.com/analyze.png", mimeType="image/png")]
|
||||
)
|
||||
def analyze(data: str) -> list:
|
||||
"""Analyze data."""
|
||||
return [{"role": "user", "content": f"Analyze: {data}"}]
|
||||
|
||||
@mcp.prompt
|
||||
def no_icon_prompt(text: str) -> list:
|
||||
"""Prompt without icon."""
|
||||
return [{"role": "user", "content": text}]
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert len(info.prompts) == 2
|
||||
|
||||
# Find the analyze prompt
|
||||
analyze_prompt = next(p for p in info.prompts if p.name == "analyze")
|
||||
assert analyze_prompt.icons is not None
|
||||
assert len(analyze_prompt.icons) == 1
|
||||
assert analyze_prompt.icons[0]["src"] == "https://example.com/analyze.png"
|
||||
|
||||
# Find the no-icon prompt
|
||||
no_icon = next(p for p in info.prompts if p.name == "no_icon_prompt")
|
||||
assert no_icon.icons is None
|
||||
|
||||
async def test_multiple_icons(self):
|
||||
"""Test that components with multiple icons extract all of them."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP(
|
||||
"MultiIconServer",
|
||||
icons=[
|
||||
Icon(
|
||||
src="https://example.com/icon-48.png",
|
||||
mimeType="image/png",
|
||||
sizes=["48x48"],
|
||||
),
|
||||
Icon(
|
||||
src="https://example.com/icon-96.png",
|
||||
mimeType="image/png",
|
||||
sizes=["96x96"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
icons=[
|
||||
Icon(src="https://example.com/tool-small.png", sizes=["24x24"]),
|
||||
Icon(src="https://example.com/tool-large.png", sizes=["48x48"]),
|
||||
]
|
||||
)
|
||||
def multi_icon_tool() -> str:
|
||||
"""Tool with multiple icons."""
|
||||
return "multi"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
# Check server icons
|
||||
assert info.icons is not None
|
||||
assert len(info.icons) == 2
|
||||
assert info.icons[0]["sizes"] == ["48x48"]
|
||||
assert info.icons[1]["sizes"] == ["96x96"]
|
||||
|
||||
# Check tool icons
|
||||
assert len(info.tools) == 1
|
||||
assert info.tools[0].icons is not None
|
||||
assert len(info.tools[0].icons) == 2
|
||||
assert info.tools[0].icons[0]["sizes"] == ["24x24"]
|
||||
assert info.tools[0].icons[1]["sizes"] == ["48x48"]
|
||||
|
||||
async def test_data_uri_icons(self):
|
||||
"""Test that data URI icons are extracted correctly."""
|
||||
from mcp.types import Icon
|
||||
|
||||
data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
|
||||
mcp = FastMCP("DataURIServer")
|
||||
|
||||
@mcp.tool(icons=[Icon(src=data_uri, mimeType="image/png")])
|
||||
def data_uri_tool() -> str:
|
||||
"""Tool with data URI icon."""
|
||||
return "data"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
|
||||
assert len(info.tools) == 1
|
||||
assert info.tools[0].icons is not None
|
||||
assert info.tools[0].icons[0]["src"] == data_uri
|
||||
assert info.tools[0].icons[0]["mimeType"] == "image/png"
|
||||
|
||||
async def test_icons_in_fastmcp_v1(self):
|
||||
"""Test that icons are extracted from FastMCP 1.x servers."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP1x("Icon1xServer")
|
||||
|
||||
@mcp.tool(
|
||||
icons=[Icon(src="https://example.com/v1-tool.png", mimeType="image/png")]
|
||||
)
|
||||
def v1_tool() -> str:
|
||||
"""Tool in v1 server."""
|
||||
return "v1"
|
||||
|
||||
info = await inspect_fastmcp_v1(mcp)
|
||||
|
||||
assert len(info.tools) == 1
|
||||
# v1 servers should also extract icons if present
|
||||
if info.tools[0].icons is not None:
|
||||
assert info.tools[0].icons[0]["src"] == "https://example.com/v1-tool.png"
|
||||
|
||||
async def test_icons_in_formatted_output(self):
|
||||
"""Test that icons appear in formatted JSON output."""
|
||||
from mcp.types import Icon
|
||||
|
||||
mcp = FastMCP(
|
||||
"FormattedIconServer",
|
||||
website_url="https://example.com",
|
||||
icons=[Icon(src="https://example.com/server.png", mimeType="image/png")],
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
icons=[Icon(src="https://example.com/tool.png", mimeType="image/png")]
|
||||
)
|
||||
def icon_tool() -> str:
|
||||
"""Tool with icon."""
|
||||
return "icon"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
json_bytes = await format_fastmcp_info(info)
|
||||
|
||||
import json
|
||||
|
||||
data = json.loads(json_bytes)
|
||||
|
||||
# Check server icons in formatted output
|
||||
assert data["server"]["website_url"] == "https://example.com"
|
||||
assert data["server"]["icons"] is not None
|
||||
assert len(data["server"]["icons"]) == 1
|
||||
assert data["server"]["icons"][0]["src"] == "https://example.com/server.png"
|
||||
|
||||
# Check tool icons in formatted output
|
||||
assert len(data["tools"]) == 1
|
||||
assert data["tools"][0]["icons"] is not None
|
||||
assert len(data["tools"][0]["icons"]) == 1
|
||||
assert data["tools"][0]["icons"][0]["src"] == "https://example.com/tool.png"
|
||||
|
||||
async def test_icons_always_present_in_json(self):
|
||||
"""Test that icons and website_url fields are always present in JSON, even when None."""
|
||||
mcp = FastMCP("AlwaysPresentServer")
|
||||
|
||||
@mcp.tool
|
||||
def no_icon() -> str:
|
||||
"""Tool without icon."""
|
||||
return "none"
|
||||
|
||||
info = await inspect_fastmcp(mcp)
|
||||
json_bytes = await format_fastmcp_info(info)
|
||||
|
||||
import json
|
||||
|
||||
data = json.loads(json_bytes)
|
||||
|
||||
# Fields should always be present, even when None
|
||||
assert "website_url" in data["server"]
|
||||
assert "icons" in data["server"]
|
||||
assert data["server"]["website_url"] is None
|
||||
assert data["server"]["icons"] is None
|
||||
|
||||
assert len(data["tools"]) == 1
|
||||
assert "icons" in data["tools"][0]
|
||||
assert data["tools"][0]["icons"] is None
|
||||
|
||||
|
||||
class TestFormatFunctions:
|
||||
"""Tests for the formatting functions."""
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue