Skip to content

Commit c386fea

Browse files
feat: add title field support to FastMCP
Add optional title field to tools, resources, prompts, and resource templates in FastMCP to enable human-readable display names. Also includes a utility function get_display_name() for consistent title/name precedence handling. - Add title parameter to @tool, @resource, and @prompt decorators - Add title field to Tool, Resource, Prompt, and ResourceTemplate models - Implement get_display_name() utility with proper precedence rules - Add comprehensive tests for title functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 994b102 commit c386fea

File tree

10 files changed

+281
-5
lines changed

10 files changed

+281
-5
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class Prompt(BaseModel):
5858
"""A prompt template that can be rendered with parameters."""
5959

6060
name: str = Field(description="Name of the prompt")
61+
title: str | None = Field(None, description="Human-readable title of the prompt")
6162
description: str | None = Field(None, description="Description of what the prompt does")
6263
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
6364
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
@@ -67,6 +68,7 @@ def from_function(
6768
cls,
6869
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
6970
name: str | None = None,
71+
title: str | None = None,
7072
description: str | None = None,
7173
) -> "Prompt":
7274
"""Create a Prompt from a function.
@@ -103,6 +105,7 @@ def from_function(
103105

104106
return cls(
105107
name=func_name,
108+
title=title,
106109
description=description or fn.__doc__ or "",
107110
arguments=arguments,
108111
fn=fn,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Resource(BaseModel, abc.ABC):
2121

2222
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(default=..., description="URI of the resource")
2323
name: str | None = Field(description="Name of the resource", default=None)
24+
title: str | None = Field(description="Human-readable title of the resource", default=None)
2425
description: str | None = Field(description="Description of the resource", default=None)
2526
mime_type: str = Field(
2627
default="text/plain",

src/mcp/server/fastmcp/resources/resource_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def add_template(
5151
fn: Callable[..., Any],
5252
uri_template: str,
5353
name: str | None = None,
54+
title: str | None = None,
5455
description: str | None = None,
5556
mime_type: str | None = None,
5657
) -> ResourceTemplate:
@@ -59,6 +60,7 @@ def add_template(
5960
fn,
6061
uri_template=uri_template,
6162
name=name,
63+
title=title,
6264
description=description,
6365
mime_type=mime_type,
6466
)

src/mcp/server/fastmcp/resources/templates.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ResourceTemplate(BaseModel):
1717

1818
uri_template: str = Field(description="URI template with parameters (e.g. weather://{city}/current)")
1919
name: str = Field(description="Name of the resource")
20+
title: str | None = Field(description="Human-readable title of the resource", default=None)
2021
description: str | None = Field(description="Description of what the resource does")
2122
mime_type: str = Field(default="text/plain", description="MIME type of the resource content")
2223
fn: Callable[..., Any] = Field(exclude=True)
@@ -28,6 +29,7 @@ def from_function(
2829
fn: Callable[..., Any],
2930
uri_template: str,
3031
name: str | None = None,
32+
title: str | None = None,
3133
description: str | None = None,
3234
mime_type: str | None = None,
3335
) -> ResourceTemplate:
@@ -45,6 +47,7 @@ def from_function(
4547
return cls(
4648
uri_template=uri_template,
4749
name=func_name,
50+
title=title,
4851
description=description or fn.__doc__ or "",
4952
mime_type=mime_type or "text/plain",
5053
fn=fn,
@@ -71,6 +74,7 @@ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
7174
return FunctionResource(
7275
uri=uri, # type: ignore
7376
name=self.name,
77+
title=self.title,
7478
description=self.description,
7579
mime_type=self.mime_type,
7680
fn=lambda: result, # Capture result in closure

src/mcp/server/fastmcp/resources/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def from_function(
7272
fn: Callable[..., Any],
7373
uri: str,
7474
name: str | None = None,
75+
title: str | None = None,
7576
description: str | None = None,
7677
mime_type: str | None = None,
7778
) -> "FunctionResource":
@@ -86,6 +87,7 @@ def from_function(
8687
return cls(
8788
uri=AnyUrl(uri),
8889
name=func_name,
90+
title=title,
8991
description=description or fn.__doc__ or "",
9092
mime_type=mime_type or "text/plain",
9193
fn=fn,

src/mcp/server/fastmcp/server.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ async def list_tools(self) -> list[MCPTool]:
237237
return [
238238
MCPTool(
239239
name=info.name,
240+
title=info.title,
240241
description=info.description,
241242
inputSchema=info.parameters,
242243
annotations=info.annotations,
@@ -270,6 +271,7 @@ async def list_resources(self) -> list[MCPResource]:
270271
MCPResource(
271272
uri=resource.uri,
272273
name=resource.name or "",
274+
title=resource.title,
273275
description=resource.description,
274276
mimeType=resource.mime_type,
275277
)
@@ -282,6 +284,7 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]:
282284
MCPResourceTemplate(
283285
uriTemplate=template.uri_template,
284286
name=template.name,
287+
title=template.title,
285288
description=template.description,
286289
)
287290
for template in templates
@@ -305,6 +308,7 @@ def add_tool(
305308
self,
306309
fn: AnyFunction,
307310
name: str | None = None,
311+
title: str | None = None,
308312
description: str | None = None,
309313
annotations: ToolAnnotations | None = None,
310314
) -> None:
@@ -316,14 +320,16 @@ def add_tool(
316320
Args:
317321
fn: The function to register as a tool
318322
name: Optional name for the tool (defaults to function name)
323+
title: Optional human-readable title for the tool
319324
description: Optional description of what the tool does
320325
annotations: Optional ToolAnnotations providing additional tool information
321326
"""
322-
self._tool_manager.add_tool(fn, name=name, description=description, annotations=annotations)
327+
self._tool_manager.add_tool(fn, name=name, title=title, description=description, annotations=annotations)
323328

324329
def tool(
325330
self,
326331
name: str | None = None,
332+
title: str | None = None,
327333
description: str | None = None,
328334
annotations: ToolAnnotations | None = None,
329335
) -> Callable[[AnyFunction], AnyFunction]:
@@ -335,6 +341,7 @@ def tool(
335341
336342
Args:
337343
name: Optional name for the tool (defaults to function name)
344+
title: Optional human-readable title for the tool
338345
description: Optional description of what the tool does
339346
annotations: Optional ToolAnnotations providing additional tool information
340347
@@ -360,7 +367,7 @@ async def async_tool(x: int, context: Context) -> str:
360367
)
361368

362369
def decorator(fn: AnyFunction) -> AnyFunction:
363-
self.add_tool(fn, name=name, description=description, annotations=annotations)
370+
self.add_tool(fn, name=name, title=title, description=description, annotations=annotations)
364371
return fn
365372

366373
return decorator
@@ -396,6 +403,7 @@ def resource(
396403
uri: str,
397404
*,
398405
name: str | None = None,
406+
title: str | None = None,
399407
description: str | None = None,
400408
mime_type: str | None = None,
401409
) -> Callable[[AnyFunction], AnyFunction]:
@@ -413,6 +421,7 @@ def resource(
413421
Args:
414422
uri: URI for the resource (e.g. "resource://my-resource" or "resource://{param}")
415423
name: Optional name for the resource
424+
title: Optional human-readable title for the resource
416425
description: Optional description of the resource
417426
mime_type: Optional MIME type for the resource
418427
@@ -462,6 +471,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
462471
fn=fn,
463472
uri_template=uri,
464473
name=name,
474+
title=title,
465475
description=description,
466476
mime_type=mime_type,
467477
)
@@ -471,6 +481,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
471481
fn=fn,
472482
uri=uri,
473483
name=name,
484+
title=title,
474485
description=description,
475486
mime_type=mime_type,
476487
)
@@ -487,11 +498,14 @@ def add_prompt(self, prompt: Prompt) -> None:
487498
"""
488499
self._prompt_manager.add_prompt(prompt)
489500

490-
def prompt(self, name: str | None = None, description: str | None = None) -> Callable[[AnyFunction], AnyFunction]:
501+
def prompt(
502+
self, name: str | None = None, title: str | None = None, description: str | None = None
503+
) -> Callable[[AnyFunction], AnyFunction]:
491504
"""Decorator to register a prompt.
492505
493506
Args:
494507
name: Optional name for the prompt (defaults to function name)
508+
title: Optional human-readable title for the prompt
495509
description: Optional description of what the prompt does
496510
497511
Example:
@@ -529,7 +543,7 @@ async def analyze_file(path: str) -> list[Message]:
529543
)
530544

531545
def decorator(func: AnyFunction) -> AnyFunction:
532-
prompt = Prompt.from_function(func, name=name, description=description)
546+
prompt = Prompt.from_function(func, name=name, title=title, description=description)
533547
self.add_prompt(prompt)
534548
return func
535549

@@ -831,6 +845,7 @@ async def list_prompts(self) -> list[MCPPrompt]:
831845
return [
832846
MCPPrompt(
833847
name=prompt.name,
848+
title=prompt.title,
834849
description=prompt.description,
835850
arguments=[
836851
MCPPromptArgument(

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Tool(BaseModel):
2222

2323
fn: Callable[..., Any] = Field(exclude=True)
2424
name: str = Field(description="Name of the tool")
25+
title: str | None = Field(None, description="Human-readable title of the tool")
2526
description: str = Field(description="Description of what the tool does")
2627
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
2728
fn_metadata: FuncMetadata = Field(
@@ -36,6 +37,7 @@ def from_function(
3637
cls,
3738
fn: Callable[..., Any],
3839
name: str | None = None,
40+
title: str | None = None,
3941
description: str | None = None,
4042
context_kwarg: str | None = None,
4143
annotations: ToolAnnotations | None = None,
@@ -69,6 +71,7 @@ def from_function(
6971
return cls(
7072
fn=fn,
7173
name=func_name,
74+
title=title,
7275
description=func_doc,
7376
parameters=parameters,
7477
fn_metadata=func_arg_metadata,

src/mcp/server/fastmcp/tools/tool_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ def add_tool(
4646
self,
4747
fn: Callable[..., Any],
4848
name: str | None = None,
49+
title: str | None = None,
4950
description: str | None = None,
5051
annotations: ToolAnnotations | None = None,
5152
) -> Tool:
5253
"""Add a tool to the server."""
53-
tool = Tool.from_function(fn, name=name, description=description, annotations=annotations)
54+
tool = Tool.from_function(fn, name=name, title=title, description=description, annotations=annotations)
5455
existing = self._tools.get(tool.name)
5556
if existing:
5657
if self.warn_on_duplicate_tools:

src/mcp/shared/metadata_utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Utility functions for working with metadata in MCP types."""
2+
3+
from mcp.types import Implementation, Prompt, Resource, ResourceTemplate, Tool
4+
5+
6+
def get_display_name(obj: Tool | Resource | Prompt | ResourceTemplate | Implementation) -> str:
7+
"""
8+
Get the display name for an MCP object with proper precedence.
9+
10+
For tools: title > annotations.title > name
11+
For other objects: title > name
12+
13+
Args:
14+
obj: An MCP object with name and optional title fields
15+
16+
Returns:
17+
The display name to use for UI presentation
18+
"""
19+
if isinstance(obj, Tool):
20+
# Tools have special precedence: title > annotations.title > name
21+
if hasattr(obj, "title") and obj.title is not None:
22+
return obj.title
23+
if obj.annotations and hasattr(obj.annotations, "title") and obj.annotations.title is not None:
24+
return obj.annotations.title
25+
return obj.name
26+
else:
27+
# All other objects: title > name
28+
if hasattr(obj, "title") and obj.title is not None:
29+
return obj.title
30+
return obj.name

0 commit comments

Comments
 (0)