@@ -126,7 +126,7 @@ def make_everything_fastmcp() -> FastMCP:
126
126
mcp = FastMCP (name = "EverythingServer" )
127
127
128
128
# 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" )
130
130
async def tool_with_progress (message : str , ctx : Context , steps : int = 3 ) -> str :
131
131
await ctx .info (f"Starting processing of '{ message } ' with { steps } steps" )
132
132
@@ -143,12 +143,12 @@ async def tool_with_progress(message: str, ctx: Context, steps: int = 3) -> str:
143
143
return f"Processed '{ message } ' in { steps } steps"
144
144
145
145
# Simple tool for basic functionality
146
- @mcp .tool (description = "A simple echo tool" )
146
+ @mcp .tool (description = "A simple echo tool" , title = "Echo Tool" )
147
147
def echo (mes
A93C
sage : str ) -> str :
148
148
return f"Echo: { message } "
149
149
150
150
# 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" )
152
152
async def sampling_tool (prompt : str , ctx : Context ) -> str :
153
153
await ctx .info (f"Requesting sampling for prompt: { prompt } " )
154
154
@@ -167,7 +167,7 @@ async def sampling_tool(prompt: str, ctx: Context) -> str:
167
167
return f"Sampling result: { str (result .content )[:100 ]} ..."
168
168
169
169
# 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" )
171
171
async def notification_tool (message : str , ctx : Context ) -> str :
172
172
# Send different log levels
173
173
await ctx .debug ("Debug: Starting notification tool" )
@@ -188,35 +188,36 @@ def get_static_info() -> str:
188
188
static_resource = FunctionResource (
189
189
uri = AnyUrl ("resource://static/info" ),
190
190
name = "Static Info" ,
191
+ title = "Static Information" ,
191
192
description = "Static information resource" ,
192
193
fn = get_static_info ,
193
194
)
194
195
mcp .add_resource (static_resource )
195
196
196
197
# Resource - dynamic function
197
- @mcp .resource ("resource://dynamic/{category}" )
198
+ @mcp .resource("resource://dynamic/{category}" , title = "Dynamic Resource" )
198
199
def dynamic_resource (category : str ) -> str :
199
200
return f"Dynamic resource content for category: { category } "
200
201
201
202
# Resource template
202
- @mcp .resource ("resource://template/{id}/data" )
203
+ @mcp .resource ("resource://template/{id}/data" , title = "Template Resource" )
203
204
def template_resource (id : str ) -> str :
204
205
return f"Template resource data for ID: { id } "
205
206
206
207
# Prompt - simple
207
- @mcp .prompt (description = "A simple prompt" )
208
+ @mcp .prompt (description = "A simple prompt" , title = "Simple Prompt" )
208
209
def simple_prompt (topic : str ) -> str :
209
210
return f"Tell me about { topic } "
210
211
211
212
# Prompt - complex with multiple messages
212
- @mcp .prompt (description = "Complex prompt with context" )
213
+ @mcp .prompt (description = "Complex prompt with context" , title = "Complex Prompt" )
213
214
def complex_prompt (user_query : str , context : str = "general" ) -> str :
214
215
# For simplicity, return a single string that incorporates the context
215
216
# Since FastMCP doesn't support system messages in the same way
216
217
return f"Context: { context } . Query: { user_query } "
217
218
218
219
# Resource template with completion support
219
- @mcp .resource ("github://repos/{owner}/{repo}" )
220
+ @mcp .resource ("github://repos/{owner}/{repo}" , title = "GitHub Repository" )
220
221
def github_repo_resource (owner : str , repo : str ) -> str :
221
222
return f"Repository: { owner } /{ repo } "
222
223
@@ -250,7 +251,7 @@ async def handle_completion(
250
251
return Completion (values = [], total = 0 , hasMore = False )
251
252
252
253
# 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" )
254
255
def echo_headers (ctx : Context [Any , Any , Request ]) -> str :
255
256
"""Returns the request headers as JSON."""
256
257
headers_info = {}
@@ -260,7 +261,7 @@ def echo_headers(ctx: Context[Any, Any, Request]) -> str:
260
261
return json .dumps (headers_info )
261
262
262
263
# 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" )
264
265
def echo_context (custom_request_id : str , ctx : Context [Any , Any , Request ]) -> str :
265
266
"""Returns request context including headers and custom data."""
266
267
context_data = {
@@ -277,7 +278,7 @@ def echo_context(custom_request_id: str, ctx: Context[Any, Any, Request]) -> str
277
278
return json .dumps (context_data )
278
279
279
280
# 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" )
281
282
async def book_restaurant (
282
283
date : str ,
283
284
time : str ,
@@ -1055,3 +1056,77 @@ async def elicitation_callback(context, params):
1055
1056
assert isinstance (tool_result .content [0 ], TextContent )
1056
1057
# # The test should only succeed with the successful elicitation response
1057
1058
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 ]
0 commit comments