@@ -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 (message : 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 ]
0 commit comments