Skip to content

Commit c1789be

Browse files
authored
Merge branch 'main' into add_root_notification
2 parents 235dbc2 + 80c0d23 commit c1789be

File tree

25 files changed

+705
-18
lines changed

25 files changed

+705
-18
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,41 @@ def debug_error(error: str) -> list[base.Message]:
516516
_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_
517517
<!-- /snippet-source -->
518518

519+
### Icons
520+
521+
MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:
522+
523+
```python
524+
from mcp.server.fastmcp import FastMCP, Icon
525+
526+
# Create an icon from a file path or URL
527+
icon = Icon(
528+
src="icon.png",
529+
mimeType="image/png",
530+
sizes="64x64"
531+
)
532+
533+
# Add icons to server
534+
mcp = FastMCP(
535+
"My Server",
536+
website_url="https://example.com",
537+
icons=[icon]
538+
)
539+
540+
# Add icons to tools, resources, and prompts
541+
@mcp.tool(icons=[icon])
542+
def my_tool():
543+
"""Tool with an icon."""
544+
return "result"
545+
546+
@mcp.resource("demo://resource", icons=[icon])
547+
def my_resource():
548+
"""Resource with an icon."""
549+
return "content"
550+
```
551+
552+
_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/fastmcp/icons_demo.py)_
553+
519554
### Images
520555

521556
FastMCP provides an `Image` class that automatically handles image data:
@@ -747,6 +782,8 @@ async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerS
747782
_Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/elicitation.py)_
748783
<!-- /snippet-source -->
749784

785+
Elicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms.
786+
750787
The `elicit()` method returns an `ElicitationResult` with:
751788

752789
- `action`: "accept", "decline", or "cancel"
@@ -896,6 +933,8 @@ The FastMCP server instance accessible via `ctx.fastmcp` provides access to serv
896933

897934
- `ctx.fastmcp.name` - The server's name as defined during initialization
898935
- `ctx.fastmcp.instructions` - Server instructions/description provided to clients
936+
- `ctx.fastmcp.website_url` - Optional website URL for the server
937+
- `ctx.fastmcp.icons` - Optional list of icons for UI display
899938
- `ctx.fastmcp.settings` - Complete server configuration object containing:
900939
- `debug` - Debug mode flag
901940
- `log_level` - Current logging level

examples/clients/simple-auth-client/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ mcp> quit
7171
## Configuration
7272

7373
- `MCP_SERVER_PORT` - Server URL (default: 8000)
74-
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`
74+
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable-http` (default) or `sse`

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def get_state(self):
150150
class SimpleAuthClient:
151151
"""Simple MCP client with auth support."""
152152

153-
def __init__(self, server_url: str, transport_type: str = "streamable_http"):
153+
def __init__(self, server_url: str, transport_type: str = "streamable-http"):
154154
self.server_url = server_url
155155
self.transport_type = transport_type
156156
self.session: ClientSession | None = None
@@ -334,10 +334,10 @@ async def main():
334334
# Default server URL - can be overridden with environment variable
335335
# Most MCP streamable HTTP servers use /mcp as the endpoint
336336
server_url = os.getenv("MCP_SERVER_PORT", 8000)
337-
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")
337+
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable-http")
338338
server_url = (
339339
f"http://localhost:{server_url}/mcp"
340-
if transport_type == "streamable_http"
340+
if transport_type == "streamable-http"
341341
else f"http://localhost:{server_url}/sse"
342342
)
343343

examples/fastmcp/icons_demo.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
FastMCP Icons Demo Server
3+
4+
Demonstrates using icons with tools, resources, prompts, and implementation.
5+
"""
6+
7+
import base64
8+
from pathlib import Path
9+
10+
from mcp.server.fastmcp import FastMCP, Icon
11+
12+
# Load the icon file and convert to data URI
13+
icon_path = Path(__file__).parent / "mcp.png"
14+
icon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()
15+
icon_data_uri = f"data:image/png;base64,{icon_data}"
16+
17+
icon_data = Icon(src=icon_data_uri, mimeType="image/png", sizes=["64x64"])
18+
19+
# Create server with icons in implementation
20+
mcp = FastMCP("Icons Demo Server", website_url="https://github.com/modelcontextprotocol/python-sdk", icons=[icon_data])
21+
22+
23+
@mcp.tool(icons=[icon_data])
24+
def demo_tool(message: str) -> str:
25+
"""A demo tool with an icon."""
26+
return message
27+
28+
29+
@mcp.resource("demo://readme", icons=[icon_data])
30+
def readme_resource() -> str:
31+
"""A demo resource with an icon"""
32+
return "This resource has an icon"
33+
34+
35+
@mcp.prompt("prompt_with_icon", icons=[icon_data])
36+
def prompt_with_icon(text: str) -> str:
37+
"""A demo prompt with an icon"""
38+
return text
39+
40+
41+
@mcp.tool(
42+
icons=[
43+
Icon(src=icon_data_uri, mimeType="image/png", sizes=["16x16"]),
44+
Icon(src=icon_data_uri, mimeType="image/png", sizes=["32x32"]),
45+
Icon(src=icon_data_uri, mimeType="image/png", sizes=["64x64"]),
46+
]
47+
)
48+
def multi_icon_tool(action: str) -> str:
49+
"""A tool demonstrating multiple icons."""
50+
return "multi_icon_tool"
51+
52+
53+
if __name__ == "__main__":
54+
# Run the server
55+
mcp.run()

examples/fastmcp/mcp.png

2.53 KB
Loading

examples/servers/simple-auth/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --tra
4343
```bash
4444
cd examples/clients/simple-auth-client
4545
# Start client with streamable HTTP
46-
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable_http uv run mcp-simple-auth-client
46+
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
4747
```
4848

4949
## How It Works
@@ -101,7 +101,7 @@ uv run mcp-simple-auth-legacy --port=8002
101101
```bash
102102
# Test with client (will automatically fall back to legacy discovery)
103103
cd examples/clients/simple-auth-client
104-
MCP_SERVER_PORT=8002 MCP_TRANSPORT_TYPE=streamable_http uv run mcp-simple-auth-client
104+
MCP_SERVER_PORT=8002 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
105105
```
106106

107107
The client will:

src/mcp/client/stdio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ async def stdout_reader():
154154
try:
155155
message = types.JSONRPCMessage.model_validate_json(line)
156156
except Exception as exc:
157+
logger.exception("Failed to parse JSONRPC message from server")
157158
await read_stream_writer.send(exc)
158159
continue
159160

src/mcp/server/auth/handlers/register.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async def handle(self, request: Request) -> Response:
6868
),
6969
status_code=400,
7070
)
71-
if set(client_metadata.grant_types) != {"authorization_code", "refresh_token"}:
71+
if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
7272
return PydanticJSONResponse(
7373
content=RegistrationErrorResponse(
7474
error="invalid_client_metadata",
@@ -77,6 +77,17 @@ async def handle(self, request: Request) -> Response:
7777
status_code=400,
7878
)
7979

80+
# The MCP spec requires servers to use the authorization `code` flow
81+
# with PKCE
82+
if "code" not in client_metadata.response_types:
83+
return PydanticJSONResponse(
84+
content=RegistrationErrorResponse(
85+
error="invalid_client_metadata",
86+
error_description="response_types must include 'code' for authorization_code grant",
87+
),
88+
status_code=400,
89+
)
90+
8091
client_id_issued_at = int(time.time())
8192
client_secret_expires_at = (
8293
client_id_issued_at + self.options.client_secret_expiry_seconds

src/mcp/server/fastmcp/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
from importlib.metadata import version
44

5+
from mcp.types import Icon
6+
57
from .server import Context, FastMCP
68
from .utilities.types import Audio, Image
79

810
__version__ = version("mcp")
9-
__all__ = ["FastMCP", "Context", "Image", "Audio"]
11+
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]

src/mcp/server/fastmcp/prompts/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from mcp.server.fastmcp.utilities.context_injection import find_context_parameter, inject_context
1313
from mcp.server.fastmcp.utilities.func_metadata import func_metadata
14-
from mcp.types import ContentBlock, TextContent
14+
from mcp.types import ContentBlock, Icon, TextContent
1515

1616
if TYPE_CHECKING:
1717
from mcp.server.fastmcp.server import Context
@@ -71,6 +71,7 @@ class Prompt(BaseModel):
7171
description: str | None = Field(None, description="Description of what the prompt does")
7272
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
7373
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
74+
icons: list[Icon] | None = Field(default=None, description="Optional list of icons for this prompt")
7475
context_kwarg: str | None = Field(None, description="Name of the kwarg that should receive context", exclude=True)
7576

7677
@classmethod
@@ -80,6 +81,7 @@ def from_function(
8081
name: str | None = None,
8182
title: str | None = None,
8283
description: str | None = None,
84+
icons: list[Icon] | None = None,
8385
context_kwarg: str | None = None,
8486
) -> Prompt:
8587
"""Create a Prompt from a function.
@@ -128,6 +130,7 @@ def from_function(
128130
description=description or fn.__doc__ or "",
129131
arguments=arguments,
130132
fn=fn,
133+
icons=icons,
131134
context_kwarg=context_kwarg,
132135
)
133136

0 commit comments

Comments
 (0)