Skip to content

Commit 91722aa

Browse files
authored
Merge branch 'main' into fix-test-with-snapshot
2 parents 0d8595c + d0443a1 commit 91722aa

File tree

41 files changed

+2554
-213
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2554
-213
lines changed

.github/workflows/check-lock.yml

Lines changed: 0 additions & 25 deletions
This file was deleted.

.github/workflows/shared.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525

2626
test:
2727
runs-on: ${{ matrix.os }}
28+
timeout-minutes: 10
2829
strategy:
2930
matrix:
3031
python-version: ["3.10", "3.11", "3.12", "3.13"]

README.md

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
- [Prompts](#prompts)
3131
- [Images](#images)
3232
- [Context](#context)
33+
- [Completions](#completions)
34+
- [Elicitation](#elicitation)
35+
- [Authentication](#authentication)
3336
- [Running Your Server](#running-your-server)
3437
- [Development Mode](#development-mode)
3538
- [Claude Desktop Integration](#claude-desktop-integration)
@@ -73,7 +76,7 @@ The Model Context Protocol allows applications to provide context for LLMs in a
7376

7477
### Adding MCP to your python project
7578

76-
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.
79+
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.
7780

7881
If you haven't created a uv-managed project yet, create one:
7982

@@ -209,13 +212,13 @@ from mcp.server.fastmcp import FastMCP
209212
mcp = FastMCP("My App")
210213

211214

212-
@mcp.resource("config://app")
215+
@mcp.resource("config://app", title="Application Configuration")
213216
def get_config() -> str:
214217
"""Static configuration data"""
215218
return "App configuration here"
216219

217220

218-
@mcp.resource("users://{user_id}/profile")
221+
@mcp.resource("users://{user_id}/profile", title="User Profile")
219222
def get_user_profile(user_id: str) -> str:
220223
"""Dynamic user data"""
221224
return f"Profile data for user {user_id}"
@@ -232,13 +235,13 @@ from mcp.server.fastmcp import FastMCP
232235
mcp = FastMCP("My App")
233236

234237

235-
@mcp.tool()
238+
@mcp.tool(title="BMI Calculator")
236239
def calculate_bmi(weight_kg: float, height_m: float) -> float:
237240
"""Calculate BMI given weight in kg and height in meters"""
238241
return weight_kg / (height_m**2)
239242

240243

241-
@mcp.tool()
244+
@mcp.tool(title="Weather Fetcher")
242245
async def fetch_weather(city: str) -> str:
243246
"""Fetch current weather for a city"""
244247
async with httpx.AsyncClient() as client:
@@ -257,12 +260,12 @@ from mcp.server.fastmcp.prompts import base
257260
mcp = FastMCP("My App")
258261

259262

260-
@mcp.prompt()
263+
@mcp.prompt(title="Code Review")
261264
def review_code(code: str) -> str:
262265
return f"Please review this code:\n\n{code}"
263266

264267

265-
@mcp.prompt()
268+
@mcp.prompt(title="Debug Assistant")
266269
def debug_error(error: str) -> list[base.Message]:
267270
return [
268271
base.UserMessage("I'm seeing this error:"),
@@ -310,6 +313,112 @@ async def long_task(files: list[str], ctx: Context) -> str:
310313
return "Processing complete"
311314
```
312315

316+
### Completions
317+
318+
MCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values:
319+
320+
Client usage:
321+
```python
322+
from mcp.client.session import ClientSession
323+
from mcp.types import ResourceTemplateReference
324+
325+
326+
async def use_completion(session: ClientSession):
327+
# Complete without context
328+
result = await session.complete(
329+
ref=ResourceTemplateReference(
330+
type="ref/resource", uri="github://repos/{owner}/{repo}"
331+
),
332+
argument={"name": "owner", "value": "model"},
333+
)
334+
335+
# Complete with context - repo suggestions based on owner
336+
result = await session.complete(
337+
ref=ResourceTemplateReference(
338+
type="ref/resource", uri="github://repos/{owner}/{repo}"
339+
),
340+
argument={"name": "repo", "value": "test"},
341+
context_arguments={"owner": "modelcontextprotocol"},
342+
)
343+
```
344+
345+
Server implementation:
346+
```python
347+
from mcp.server import Server
348+
from mcp.types import (
349+
Completion,
350+
CompletionArgument,
351+
CompletionContext,
352+
PromptReference,
353+
ResourceTemplateReference,
354+
)
355+
356+
server = Server("example-server")
357+
358+
359+
@server.completion()
360+
async def handle_completion(
361+
ref: PromptReference | ResourceTemplateReference,
362+
argument: CompletionArgument,
363+
context: CompletionContext | None,
364+
) -> Completion | None:
365+
if isinstance(ref, ResourceTemplateReference):
366+
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
367+
# Use context to provide owner-specific repos
368+
if context and context.arguments:
369+
owner = context.arguments.get("owner")
370+
if owner == "modelcontextprotocol":
371+
repos = ["python-sdk", "typescript-sdk", "specification"]
372+
# Filter based on partial input
373+
filtered = [r for r in repos if r.startswith(argument.value)]
374+
return Completion(values=filtered)
375+
return None
376+
```
377+
### Elicitation
378+
379+
Request additional information from users during tool execution:
380+
381+
```python
382+
from mcp.server.fastmcp import FastMCP, Context
383+
from mcp.server.elicitation import (
384+
AcceptedElicitation,
385+
DeclinedElicitation,
386+
CancelledElicitation,
387+
)
388+
from pydantic import BaseModel, Field
389+
390+
mcp = FastMCP("Booking System")
391+
392+
393+
@mcp.tool()
394+
async def book_table(date: str, party_size: int, ctx: Context) -> str:
395+
"""Book a table with confirmation"""
396+
397+
# Schema must only contain primitive types (str, int, float, bool)
398+
class ConfirmBooking(BaseModel):
399+
confirm: bool = Field(description="Confirm booking?")
400+
notes: str = Field(default="", description="Special requests")
401+
402+
result = await ctx.elicit(
403+
message=f"Confirm booking for {party_size} on {date}?", schema=ConfirmBooking
404+
)
405+
406+
match result:
407+
case AcceptedElicitation(data=data):
408+
if data.confirm:
409+
return f"Booked! Notes: {data.notes or 'None'}"
410+
return "Booking cancelled"
411+
case DeclinedElicitation():
412+
return "Booking declined"
413+
case CancelledElicitation():
414+
return "Booking cancelled"
415+
```
416+
417+
The `elicit()` method returns an `ElicitationResult` with:
418+
- `action`: "accept", "decline", or "cancel"
419+
- `data`: The validated response (only when accepted)
420+
- `validation_error`: Any validation error message
421+
313422
### Authentication
314423

315424
Authentication can be used by servers that want to expose tools accessing protected resources.
@@ -809,6 +918,42 @@ async def main():
809918
tool_result = await session.call_tool("echo", {"message": "hello"})
810919
```
811920

921+
### Client Display Utilities
922+
923+
When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:
924+
925+
```python
926+
from mcp.shared.metadata_utils import get_display_name
927+
from mcp.client.session import ClientSession
928+
929+
930+
async def display_tools(session: ClientSession):
931+
"""Display available tools with human-readable names"""
932+
tools_response = await session.list_tools()
933+
934+
for tool in tools_response.tools:
935+
# get_display_name() returns the title if available, otherwise the name
936+
display_name = get_display_name(tool)
937+
print(f"Tool: {display_name}")
938+
if tool.description:
939+
print(f" {tool.description}")
940+
941+
942+
async def display_resources(session: ClientSession):
943+
"""Display available resources with human-readable names"""
944+
resources_response = await session.list_resources()
945+
946+
for resource in resources_response.resources:
947+
display_name = get_display_name(resource)
948+
print(f"Resource: {display_name} ({resource.uri})")
949+
```
950+
951+
The `get_display_name()` function implements the proper precedence rules for displaying names:
952+
- For tools: `title` > `annotations.title` > `name`
953+
- For other objects: `title` > `name`
954+
955+
This ensures your client UI shows the most user-friendly names that servers provide.
956+
812957
### OAuth Authentication for Clients
813958

814959
The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async def list_tools(self) -> list[Any]:
123123
for item in tools_response:
124124
if isinstance(item, tuple) and item[0] == "tools":
125125
tools.extend(
126-
Tool(tool.name, tool.description, tool.inputSchema)
126+
Tool(tool.name, tool.description, tool.inputSchema, tool.title)
127127
for tool in item[1]
128128
)
129129

@@ -189,9 +189,14 @@ class Tool:
189189
"""Represents a tool with its properties and formatting."""
190190

191191
def __init__(
192-
self, name: str, description: str, input_schema: dict[str, Any]
192+
self,
193+
name: str,
194+
description: str,
195+
input_schema: dict[str, Any],
196+
title: str | None = None,
193197
) -> None:
194198
self.name: str = name
199+
self.title: str | None = title
195200
self.description: str = description
196201
self.input_schema: dict[str, Any] = input_schema
197202

@@ -211,13 +216,20 @@ def format_for_llm(self) -> str:
211216
arg_desc += " (required)"
212217
args_desc.append(arg_desc)
213218

214-
return f"""
215-
Tool: {self.name}
216-
Description: {self.description}
219+
# Build the formatted output with title as a separate field
220+
output = f"Tool: {self.name}\n"
221+
222+
# Add human-readable title if available
223+
if self.title:
224+
output += f"User-readable title: {self.title}\n"
225+
226+
output += f"""Description: {self.description}
217227
Arguments:
218228
{chr(10).join(args_desc)}
219229
"""
220230

231+
return output
232+
221233

222234
class LLMClient:
223235
"""Manages communication with the LLM provider."""

examples/servers/simple-prompt/mcp_simple_prompt/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async def list_prompts() -> list[types.Prompt]:
5353
return [
5454
types.Prompt(
5555
name="simple",
56+
title="Simple Assistant Prompt",
5657
description="A simple prompt that can take optional context and topic "
5758
"arguments",
5859
arguments=[

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@
55
from pydantic import AnyUrl, FileUrl
66

77
SAMPLE_RESOURCES = {
8-
"greeting": "Hello! This is a sample text resource.",
9-
"help": "This server provides a few sample text resources for testing.",
10-
"about": "This is the simple-resource MCP server implementation.",
8+
"greeting": {
9+
"content": "Hello! This is a sample text resource.",
10+
"title": "Welcome Message",
11+
},
12+
"help": {
13+
"content": "This server provides a few sample text resources for testing.",
14+
"title": "Help Documentation",
15+
},
16+
"about": {
17+
"content": "This is the simple-resource MCP server implementation.",
18+
"title": "About This Server",
19+
},
1120
}
1221

1322

@@ -28,6 +37,7 @@ async def list_resources() -> list[types.Resource]:
2837
types.Resource(
2938
uri=FileUrl(f"file:///{name}.txt"),
3039
name=name,
40+
title=SAMPLE_RESOURCES[name]["title"],
3141
description=f"A sample text resource named {name}",
3242
mimeType="text/plain",
3343
)
@@ -43,7 +53,7 @@ async def read_resource(uri: AnyUrl) -> str | bytes:
4353
if name not in SAMPLE_RESOURCES:
4454
raise ValueError(f"Unknown resource: {uri}")
4555

46-
return SAMPLE_RESOURCES[name]
56+
return SAMPLE_RESOURCES[name]["content"]
4757

4858
if transport == "sse":
4959
from mcp.server.sse import SseServerTransport

examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def main(
4141
app = Server("mcp-streamable-http-stateless-demo")
4242

4343
@app.call_tool()
44-
async def call_tool(name: str, arguments: dict) -> list[types.Content]:
44+
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
4545
ctx = app.request_context
4646
interval = arguments.get("interval", 1.0)
4747
count = arguments.get("count", 5)

examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def main(
4545
app = Server("mcp-streamable-http-demo")
4646

4747
@app.call_tool()
48-
async def call_tool(name: str, arguments: dict) -> list[types.Content]:
48+
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
4949
ctx = app.request_context
5050
interval = arguments.get("interval", 1.0)
5151
count = arguments.get("count", 5)

examples/servers/simple-tool/mcp_simple_tool/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
async def fetch_website(
99
url: str,
10-
) -> list[types.Content]:
10+
) -> list[types.ContentBlock]:
1111
headers = {
1212
"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
1313
}
@@ -29,7 +29,7 @@ def main(port: int, transport: str) -> int:
2929
app = Server("mcp-website-fetcher")
3030

3131
@app.call_tool()
32-
async def fetch_tool(name: str, arguments: dict) -> list[types.Content]:
32+
async def fetch_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
3333
if name != "fetch":
3434
raise ValueError(f"Unknown tool: {name}")
3535
if "url" not in arguments:
@@ -41,6 +41,7 @@ async def list_tools() -> list[types.Tool]:
4141
return [
4242
types.Tool(
4343
name="fetch",
44+
title="Website Fetcher",
4445
description="Fetches a website and returns its content",
4546
inputSchema={
4647
"type": "object",

0 commit comments

Comments
 (0)