Add title to tools, resources, prompts (#972)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Felix Weinberger 2025-06-17 10:53:14 -04:00 • committed by GitHub
commit a2f8766a8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 481 additions and 46 deletions

View file

@ -58,6 +58,7 @@ class Prompt(BaseModel):
"""A prompt template that can be rendered with parameters."""
name: str = Field(description="Name of the prompt")
title: str | None = Field(None, description="Human-readable title of the prompt")
description: str | None = Field(None, description="Description of what the prompt does")
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
@ -67,6 +68,7 @@ class Prompt(BaseModel):
cls,
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
name: str | None = None,
title: str | None = None,
description: str | None = None,
) -> "Prompt":
"""Create a Prompt from a function.
@ -103,6 +105,7 @@ class Prompt(BaseModel):
return cls(
name=func_name,
title=title,
description=description or fn.__doc__ or "",
arguments=arguments,
fn=fn,

View file

@ -21,6 +21,7 @@ class Resource(BaseModel, abc.ABC):
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(default=..., description="URI of the resource")
name: str | None = Field(description="Name of the resource", default=None)
title: str | None = Field(description="Human-readable title of the resource", default=None)
description: str | None = Field(description="Description of the resource", default=None)
mime_type: str = Field(
default="text/plain",

View file

@ -51,6 +51,7 @@ class ResourceManager:
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> ResourceTemplate:
@ -59,6 +60,7 @@ class ResourceManager:
fn,
uri_template=uri_template,
name=name,
title=title,
description=description,
mime_type=mime_type,
)

View file

@ -17,6 +17,7 @@ class ResourceTemplate(BaseModel):
uri_template: str = Field(description="URI template with parameters (e.g. weather://{city}/current)")
name: str = Field(description="Name of the resource")
title: str | None = Field(description="Human-readable title of the resource", default=None)
description: str | None = Field(description="Description of what the resource does")
mime_type: str = Field(default="text/plain", description="MIME type of the resource content")
fn: Callable[..., Any] = Field(exclude=True)
@ -28,6 +29,7 @@ class ResourceTemplate(BaseModel):
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> ResourceTemplate:
@ -45,6 +47,7 @@ class ResourceTemplate(BaseModel):
return cls(
uri_template=uri_template,
name=func_name,
title=title,
description=description or fn.__doc__ or "",
mime_type=mime_type or "text/plain",
fn=fn,
@ -71,6 +74,7 @@ class ResourceTemplate(BaseModel):
return FunctionResource(
uri=uri, # type: ignore
name=self.name,
title=self.title,
description=self.description,
mime_type=self.mime_type,
fn=lambda: result, # Capture result in closure

View file

@ -72,6 +72,7 @@ class FunctionResource(Resource):
fn: Callable[..., Any],
uri: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> "FunctionResource":
@ -86,6 +87,7 @@ class FunctionResource(Resource):
return cls(
uri=AnyUrl(uri),
name=func_name,
title=title,
description=description or fn.__doc__ or "",
mime_type=mime_type or "text/plain",
fn=fn,

View file

@ -237,6 +237,7 @@ class FastMCP:
return [
MCPTool(
name=info.name,
title=info.title,
description=info.description,
inputSchema=info.parameters,
annotations=info.annotations,
@ -270,6 +271,7 @@ class FastMCP:
MCPResource(
uri=resource.uri,
name=resource.name or "",
title=resource.title,
description=resource.description,
mimeType=resource.mime_type,
)
@ -282,6 +284,7 @@ class FastMCP:
MCPResourceTemplate(
uriTemplate=template.uri_template,
name=template.name,
title=template.title,
description=template.description,
)
for template in templates
@ -305,6 +308,7 @@ class FastMCP:
self,
fn: AnyFunction,
name: str | None = None,
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> None:
@ -316,14 +320,16 @@ class FastMCP:
Args:
fn: The function to register as a tool
name: Optional name for the tool (defaults to function name)
title: Optional human-readable title for the tool
description: Optional description of what the tool does
annotations: Optional ToolAnnotations providing additional tool information
"""
self._tool_manager.add_tool(fn, name=name, description=description, annotations=annotations)
self._tool_manager.add_tool(fn, name=name, title=title, description=description, annotations=annotations)
def tool(
self,
name: str | None = None,
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
@ -335,6 +341,7 @@ class FastMCP:
Args:
name: Optional name for the tool (defaults to function name)
title: Optional human-readable title for the tool
description: Optional description of what the tool does
annotations: Optional ToolAnnotations providing additional tool information
@ -360,7 +367,7 @@ class FastMCP:
)
def decorator(fn: AnyFunction) -> AnyFunction:
self.add_tool(fn, name=name, description=description, annotations=annotations)
self.add_tool(fn, name=name, title=title, description=description, annotations=annotations)
return fn
return decorator
@ -396,6 +403,7 @@ class FastMCP:
uri: str,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> Callable[[AnyFunction], AnyFunction]:
@ -413,6 +421,7 @@ class FastMCP:
Args:
uri: URI for the resource (e.g. "resource://my-resource" or "resource://{param}")
name: Optional name for the resource
title: Optional human-readable title for the resource
description: Optional description of the resource
mime_type: Optional MIME type for the resource
@ -462,6 +471,7 @@ class FastMCP:
fn=fn,
uri_template=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
)
@ -471,6 +481,7 @@ class FastMCP:
fn=fn,
uri=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
)
@ -487,11 +498,14 @@ class FastMCP:
"""
self._prompt_manager.add_prompt(prompt)
def prompt(self, name: str | None = None, description: str | None = None) -> Callable[[AnyFunction], AnyFunction]:
def prompt(
self, name: str | None = None, title: str | None = None, description: str | None = None
) -> Callable[[AnyFunction], AnyFunction]:
"""Decorator to register a prompt.
Args:
name: Optional name for the prompt (defaults to function name)
title: Optional human-readable title for the prompt
description: Optional description of what the prompt does
Example:
@ -529,7 +543,7 @@ class FastMCP:
)
def decorator(func: AnyFunction) -> AnyFunction:
prompt = Prompt.from_function(func, name=name, description=description)
prompt = Prompt.from_function(func, name=name, title=title, description=description)
self.add_prompt(prompt)
return func
@ -831,6 +845,7 @@ class FastMCP:
return [
MCPPrompt(
name=prompt.name,
title=prompt.title,
description=prompt.description,
arguments=[
MCPPromptArgument(

View file

@ -22,6 +22,7 @@ class Tool(BaseModel):
fn: Callable[..., Any] = Field(exclude=True)
name: str = Field(description="Name of the tool")
title: str | None = Field(None, description="Human-readable title of the tool")
description: str = Field(description="Description of what the tool does")
parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
fn_metadata: FuncMetadata = Field(
@ -36,6 +37,7 @@ class Tool(BaseModel):
cls,
fn: Callable[..., Any],
name: str | None = None,
title: str | None = None,
description: str | None = None,
context_kwarg: str | None = None,
annotations: ToolAnnotations | None = None,
@ -69,6 +71,7 @@ class Tool(BaseModel):
return cls(
fn=fn,
name=func_name,
title=title,
description=func_doc,
parameters=parameters,
fn_metadata=func_arg_metadata,

View file

@ -46,11 +46,12 @@ class ToolManager:
self,
fn: Callable[..., Any],
name: str | None = None,
title: str | None = None,
description: str | None = None,
annotations: ToolAnnotations | None = None,
) -> Tool:
"""Add a tool to the server."""
tool = Tool.from_function(fn, name=name, description=description, annotations=annotations)
tool = Tool.from_function(fn, name=name, title=title, description=description, annotations=annotations)
existing = self._tools.get(tool.name)
if existing:
if self.warn_on_duplicate_tools:

View file

@ -0,0 +1,45 @@
"""Utility functions for working with metadata in MCP types.
These utilities are primarily intended for client-side usage to properly display
human-readable names in user interfaces in a spec compliant way.
"""
from mcp.types import Implementation, Prompt, Resource, ResourceTemplate, Tool
def get_display_name(obj: Tool | Resource | Prompt | ResourceTemplate | Implementation) -> str:
"""
Get the display name for an MCP object with proper precedence.
This is a client-side utility function designed to help MCP clients display
human-readable names in their user interfaces. When servers provide a 'title'
field, it should be preferred over the programmatic 'name' field for display.
For tools: title > annotations.title > name
For other objects: title > name
Example:
# In a client displaying available tools
tools = await session.list_tools()
for tool in tools.tools:
display_name = get_display_name(tool)
print(f"Available tool: {display_name}")
Args:
obj: An MCP object with name and optional title fields
Returns:
The display name to use for UI presentation
"""
if isinstance(obj, Tool):
# Tools have special precedence: title > annotations.title > name
if hasattr(obj, "title") and obj.title is not None:
return obj.title
if obj.annotations and hasattr(obj.annotations, "title") and obj.annotations.title is not None:
return obj.annotations.title
return obj.name
else:
# All other objects: title > name
if hasattr(obj, "title") and obj.title is not None:
return obj.title
return obj.name

View file

@ -196,10 +196,26 @@ class EmptyResult(Result):
"""A response that indicates success but carries no data."""
class Implementation(BaseModel):
"""Describes the name and version of an MCP implementation."""
class BaseMetadata(BaseModel):
"""Base class for entities with name and optional title fields."""
name: str
"""The programmatic name of the entity."""
title: str | None = None
"""
Intended for UI and end-user contexts — optimized to be human-readable and easily understood,
even by those unfamiliar with domain-specific terminology.
If not provided, the name should be used for display (except for Tool,
where `annotations.title` should be given precedence over using `name`,
if present).
"""
class Implementation(BaseMetadata):
"""Describes the name and version of an MCP implementation."""
version: str
model_config = ConfigDict(extra="allow")
@ -382,13 +398,11 @@ class Annotations(BaseModel):
model_config = ConfigDict(extra="allow")
class Resource(BaseModel):
class Resource(BaseMetadata):
"""A known resource that the server is capable of reading."""
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
"""The URI of this resource."""
name: str
"""A human-readable name for this resource."""
description: str | None = None
"""A description of what this resource represents."""
mimeType: str | None = None
@ -409,7 +423,7 @@ class Resource(BaseModel):
model_config = ConfigDict(extra="allow")
class ResourceTemplate(BaseModel):
class ResourceTemplate(BaseMetadata):
"""A template description for resources available on the server."""
uriTemplate: str
@ -417,8 +431,6 @@ class ResourceTemplate(BaseModel):
A URI template (according to RFC 6570) that can be used to construct resource
URIs.
"""
name: str
"""A human-readable name for the type of resource this template refers to."""
description: str | None = None
"""A human-readable description of what this template is for."""
mimeType: str | None = None
@ -601,11 +613,9 @@ class PromptArgument(BaseModel):
model_config = ConfigDict(extra="allow")
class Prompt(BaseModel):
class Prompt(BaseMetadata):
"""A prompt or prompt template that the server offers."""
name: str
"""The name of the prompt or prompt template."""
description: str | None = None
"""An optional description of what this prompt provides."""
arguments: list[PromptArgument] | None = None
@ -808,11 +818,9 @@ class ToolAnnotations(BaseModel):
model_config = ConfigDict(extra="allow")
class Tool(BaseModel):
class Tool(BaseMetadata):
"""Definition for a tool the client can call."""
name: str
"""The name of the tool."""
description: str | None = None
"""A human-readable description of the tool."""
inputSchema: dict[str, Any]