8000 feat: address PR review comments for title field implementation · modelcontextprotocol/python-sdk@1a892bb · GitHub
[go: up one dir, main page]

Skip to content

Commit 1a892bb

Browse files
feat: address PR review comments for title field implementation
- Add titles to all tools, resources, and prompts in make_everything_fastmcp test fixture - Refactor simple-resource server to use single dict for resource data and titles - Add MCP specification link to metadata_utils.py documentation - Add comprehensive test_title_precedence to verify title functionality - Fix missing title field in test_tool_manager.py Tool instantiation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6fb06d7 commit 1a892bb

File tree

4 files changed

+106
-24
lines changed

4 files changed

+106
-24
lines changed

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +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.",
11-
}
12-
13-
RESOURCE_TITLES = {
14-
"greeting": "Welcome Message",
15-
"help": "Help Documentation",
16-
"about": "About This Server",
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+
},
1720
}
1821

1922

@@ -34,7 +37,7 @@ async def list_resources() -> list[types.Resource]:
3437
types.Resource(
3538
uri=FileUrl(f"file:///{name}.txt"),
3639
name=name,
37-
title=RESOURCE_TITLES.get(name),
40+
title=SAMPLE_RESOURCES[name]["title"],
3841
description=f"A sample text resource named {name}",
3942
mimeType="text/plain",
4043
)
@@ -50,7 +53,7 @@ async def read_resource(uri: AnyUrl) -> str | bytes:
5053
if name not in SAMPLE_RESOURCES:
5154
raise ValueError(f"Unknown resource: {uri}")
5255

53-
return SAMPLE_RESOURCES[name]
56+
return SAMPLE_RESOURCES[name]["content"]
5457

5558
if transport == "sse":
5659
from mcp.server.sse import SseServerTransport

src/mcp/shared/metadata_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Utility functions for working with metadata in MCP types.
22
33
These utilities are primarily intended for client-side usage to properly display
4-
human-readable names in user interfaces.
4+
human-readable names in user interfaces in a spec compliant way.
5+
6+
See https://modelcontextprotocol.io/specification and search for "Tools" for
7+
example usage of the `title` field.
58
"""
69

710
from mcp.types import Implementation, Prompt, Resource, ResourceTemplate, Tool

tests/server/fastmcp/test_integration.py

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def make_everything_fastmcp() -> FastMCP:
126126
mcp = FastMCP(name="EverythingServer")
127127

128128
# Tool with context for logging and progress
129-
@mcp.tool(description="A tool that demonstrates logging and progress")
129+
@mcp.tool(description="A tool that demonstrates logging and progress", title="Progress Tool")
130130
async def tool_with_progress(message: str, ctx: Context, steps: int = 3) -> str:
131131
await ctx.info(f"Starting processing of '{message}' with {steps} steps")
132132

@@ -143,12 +143,12 @@ async def tool_with_progress(message: str, ctx: Context, steps: int = 3) -> str:
143143
return f"Processed '{message}' in {steps} steps"
144144

145145
# Simple tool for basic functionality
146-
@mcp.tool(description="A simple echo tool")
146+
@mcp.tool(description="A simple echo tool", title="Echo Tool")
147147
def echo(mes A93C sage: str) -> str:
148148
return f"Echo: {message}"
149149

150150
# Tool with sampling capability
151-
@mcp.tool(description="A tool that uses sampling to generate content")
151+
@mcp.tool(description="A tool that uses sampling to generate content", title="Sampling Tool")
152152
async def sampling_tool(prompt: str, ctx: Context) -> str:
153153
await ctx.info(f"Requesting sampling for prompt: {prompt}")
154154

@@ -167,7 +167,7 @@ async def sampling_tool(prompt: str, ctx: Context) -> str:
167167
return f"Sampling result: {str(result.content)[:100]}..."
168168

169169
# Tool that sends notifications and logging
170-
@mcp.tool(description="A tool that demonstrates notifications and logging")
170+
@mcp.tool(description="A tool that demonstrates notifications and logging", title="Notification Tool")
171171
async def notification_tool(message: str, ctx: Context) -> str:
172172
# Send different log levels
173173
await ctx.debug("Debug: Starting notification tool")
@@ -188,35 +188,36 @@ def get_static_info() -> str:
188188
static_resource = FunctionResource(
189189
uri=AnyUrl("resource://static/info"),
190190
name="Static Info",
191+
title="Static Information",
191192
description="Static information resource",
192193
fn=get_static_info,
193194
)
194195
mcp.add_resource(static_resource)
195196

196197
# Resource - dynamic function
197-
@mcp.resource("resource://dynamic/{category}")
198+
@mcp.resource("resource://dynamic/{category}", title="Dynamic Resource")
198199
def dynamic_resource(category: str) -> str:
199200
return f"Dynamic resource content for category: {category}"
200201

201202
# Resource template
202-
@mcp.resource("resource://template/{id}/data")
203+
@mcp.resource("resource://template/{id}/data", title="Template Resource")
203204
def template_resource(id: str) -> str:
204205
return f"Template resource data for ID: {id}"
205206

206207
# Prompt - simple
207-
@mcp.prompt(description="A simple prompt")
208+
@mcp.prompt(description="A simple prompt", title="Simple Prompt")
208209
def simple_prompt(topic: str) -> str:
209210
return f"Tell me about {topic}"
210211

211212
# Prompt - complex with multiple messages
212-
@mcp.prompt(description="Complex prompt with context")
213+
@mcp.prompt(description="Complex prompt with context", title="Complex Prompt")
213214
def complex_prompt(user_query: str, context: str = "general") -> str:
214215
# For simplicity, return a single string that incorporates the context
215216
# Since FastMCP doesn't support system messages in the same way
216217
return f"Context: {context}. Query: {user_query}"
217218

218219
# Resource template with completion support
219-
@mcp.resource("github://repos/{owner}/{repo}")
220+
@mcp.resource("github://repos/{owner}/{repo}", title="GitHub Repository")
220221
def github_repo_resource(owner: str, repo: str) -> str:
221222
return f"Repository: {owner}/{repo}"
222223

@@ -250,7 +251,7 @@ async def handle_completion(
250251
return Completion(values=[], total=0, hasMore=False)
251252

252253
# Tool that echoes request headers from context
253-
@mcp.tool(description="Echo request headers from context")
254+
@mcp.tool(description="Echo request headers from context", title="Echo Headers")
254255
def echo_headers(ctx: Context[Any, Any, Request]) -> str:
255256
"""Returns the request headers as JSON."""
256257
headers_info = {}
@@ -260,7 +261,7 @@ def echo_headers(ctx: Context[Any, Any, Request]) -> str:
260261
return json.dumps(headers_info)
261262

262263
# Tool that returns full request context
263-
@mcp.tool(description="Echo request context with custom data")
264+
@mcp.tool(description="Echo request context with custom data", title="Echo Context")
264265
def echo_context(custom_request_id: str, ctx: Context[Any, Any, Request]) -> str:
265266
"""Returns request context including headers and custom data."""
266267
context_data = {
@@ -277,7 +278,7 @@ def echo_context(custom_request_id: str, ctx: Context[Any, Any, Request]) -> str
277278
return json.dumps(context_data)
278279

279280
# Restaurant booking tool with elicitation
280-
@mcp.tool(description="Book a table at a restaurant with elicitation")
281+
@mcp.tool(description="Book a table at a restaurant with elicitation", title="Restaurant Booking")
281282
async def book_restaurant(
282283
date: str,
283284
time: str,
@@ -1055,3 +1056,77 @@ async def elicitation_callback(context, params):
10551056
assert isinstance(tool_result.content[0], TextContent)
10561057
# # The test should only succeed with the successful elicitation response
10571058
assert tool_result.content[0].text == "User answered: Test User"
1059+
1060+
1061+
@pytest.mark.anyio
1062+
async def test_title_precedence(everything_server: None, everything_server_url: str) -> None:
1063+
"""Test that titles are properly returned for tools, resources, and prompts."""
1064+
from mcp.shared.metadata_utils import get_display_name
1065+
1066+
async with sse_client(everything_server_url + "/sse") as streams:
1067+
async with ClientSession(*streams) as session:
1068+
# Initialize the session
1069+
result = await session.initialize()
1070+
assert isinstance(result, InitializeResult)
1071+
1072+
# Test tools have titles
1073+
tools_result = await session.list_tools()
1074+
assert tools_result.tools
1075+
1076+
# Check specific tools have titles
1077+
tool_names_to_titles = {
1078+
"tool_with_progress": "Progress Tool",
1079+
"echo": "Echo Tool",
1080+
"sampling_tool": "Sampling Tool",
1081+
"notification_tool": "Notification Tool",
1082+
"echo_headers": "Echo Headers",
1083+
"echo_context": "Echo Context",
1084+
"book_restaurant": "Restaurant Booking",
1085+
}
1086+
1087+
for tool in tools_result.tools:
1088+
if tool.name in tool_names_to_titles:
1089+
assert tool.title == tool_names_to_titles[tool.name]
1090+
# Test get_display_name utility
1091+
assert get_display_name(tool) == tool_names_to_titles[tool.name]
1092+
1093+
# Test resources have titles
1094+
resources_result = await session.list_resources()
1095+
assert resources_result.resources
1096+
1097+
# Check specific resources have titles
1098+
static_resource = next((r for r in resources_result.resources if r.name == "Static Info"), None)
1099+
assert static_resource is not None
1100+
assert static_resource.title == "Static Information"
1101+
assert get_display_name(static_resource) == "Static Information"
1102+
1103+
# Test resource templates have titles
1104+
resource_templates = await session.list_resource_templates()
1105+
assert resource_templates.resourceTemplates
1106+
1107+
# Check specific resource templates have titles
1108+
template_uris_to_titles = {
1109+
"resource://dynamic/{category}": "Dynamic Resource",
1110+
"resource://template/{id}/data": "Template Resource",
1111+
"github://repos/{owner}/{repo}": "GitHub Repository",
1112+
}
1113+
1114+
for template in resource_templates.resourceTemplates:
1115+
if template.uriTemplate in template_uris_to_titles:
1116+
assert template.title == template_uris_to_titles[template.uriTemplate]
1117+
assert get_display_name(template) == template_uris_to_titles[template.uriTemplate]
1118+
1119+
# Test prompts have titles
1120+
prompts_result = await session.list_prompts()
1121+
assert prompts_result.prompts
1122+
1123+
# Check specific prompts have titles
1124+
prompt_names_to_titles = {
1125+
"simple_prompt": "Simple Prompt",
1126+
"complex_prompt": "Complex Prompt",
1127+
}
1128+
1129+
for prompt in prompts_result.prompts:
1130+
if prompt.name in prompt_names_to_titles:
1131+
assert prompt.title == prompt_names_to_titles[prompt.name]
1132+
assert get_display_name(prompt) == prompt_names_to_titles[prompt.name]

tests/server/fastmcp/test_tool_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class AddArguments(ArgModelBase):
4444

4545
original_tool = Tool(
4646
name="add",
47+
title=None,
4748
description="Add two numbers.",
4849
fn=add,
4950
fn_metadata=fn_metadata,

0 commit comments

Comments
 (0)
0