8000 Add title to tools, resources, prompts by felixweinberger · Pull Request #972 · modelcontextprotocol/python-sdk · GitHub
[go: up one dir, main page]

Skip to content
8000

Add title to tools, resources, prompts #972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,13 @@ from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")


@mcp.resource("config://app")
@mcp.resource("config://app", title="Application Configuration")
def get_config() -> str:
"""Static configuration data"""
return "App configuration here"


@mcp.resource("users://{user_id}/profile")
@mcp.resource("users://{user_id}/profile", title="User Profile")
def get_user_profile(user_id: str) -> str:
"""Dynamic user data"""
return f"Profile data for user {user_id}"
Expand All @@ -235,13 +235,13 @@ from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")


@mcp.tool()
@mcp.tool(title="BMI Calculator")
def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Calculate BMI given weight in kg and height in meters"""
return weight_kg / (height_m**2)


@mcp.tool()
@mcp.tool(title="Weather Fetcher")
async def fetch_weather(city: str) -> str:
"""Fetch current weather for a city"""
async with httpx.AsyncClient() as client:
Expand All @@ -260,12 +260,12 @@ from mcp.server.fastmcp.prompts import base
mcp = FastMCP("My App")


@mcp.prompt()
@mcp.prompt(title="Code Review")
def review_code(code: str) -> str:
return f"Please review this code:\n\n{code}"


@mcp.prompt()
@mcp.prompt(title="Debug Assistant")
def debug_error(error: str) -> list[base.Message]:
return [
base.UserMessage("I'm seeing this error:"),
Expand Down Expand Up @@ -918,6 +918,42 @@ async def main():
tool_result = await session.call_tool("echo", {"message": "hello"})
```

### Client Display Utilities

When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:

```python
from mcp.shared.metadata_utils import get_display_name
from mcp.client.session import ClientSession


async def display_tools(session: ClientSession):
"""Display available tools with human-readable names"""
tools_response = await session.list_tools()

for tool in tools_response.tools:
# get_display_name() returns the title if available, otherwise the name
display_name = get_display_name(tool)
print(f"Tool: {display_name}")
if tool.description:
print(f" {tool.description}")


async def display_resources(session: ClientSession):
"""Display available resources with human-readable names"""
resources_response = await session.list_resources()

for resource in resources_response.resources:
display_name = get_display_name(resource)
print(f"Resource: {display_name} ({resource.uri})")
```

The `get_display_name()` function implements the proper precedence rules for displaying names:
- For tools: `title` > `annotations.title` > `name`
- For other objects: `title` > `name`

This ensures your client UI shows the most user-friendly names that servers provide.

### OAuth Authentication for Clients

The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:
Expand Down
22 changes: 17 additions & 5 deletions examples/clients/simple-chatbot/mcp_simple_chatbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async def list_tools(self) -> list[Any]:
for item in tools_response:
if isinstance(item, tuple) and item[0] == "tools":
tools.extend(
Tool(tool.name, tool.description, tool.inputSchema)
Tool(tool.name, tool.description, tool.inputSchema, tool.title)
for tool in item[1]
)

Expand Down Expand Up @@ -189,9 +189,14 @@ class Tool:
"""Represents a tool with its properties and formatting."""

def __init__(
self, name: str, description: str, input_schema: dict[str, Any]
se E864 lf,
name: str,
description: str,
input_schema: dict[str, Any],
title: str | None = None,
) -> None:
self.name: str = name
self.title: str | None = title
self.description: str = description
self.input_schema: dict[str, Any] = input_schema

Expand All @@ -211,13 +216,20 @@ def format_for_llm(self) -> str:
arg_desc += " (required)"
args_desc.append(arg_desc)

return f"""
Tool: {self.name}
Description: {self.description}
# Build the formatted output with title as a separate field
output = f"Tool: {self.name}\n"

# Add human-readable title if available
if self.title:
output += f"User-readable title: {self.title}\n"

output += f"""Description: {self.description}
Arguments:
{chr(10).join(args_desc)}
"""

return output


class LLMClient:
"""Manages communication with the LLM provider."""
Expand Down
1 change: 1 addition & 0 deletions examples/servers/simple-prompt/mcp_simple_prompt/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="simple",
title="Simple Assistant Prompt",
description="A simple prompt that can take optional context and topic "
"arguments",
arguments=[
Expand Down
18 changes: 14 additions & 4 deletions examples/servers/simple-resource/mcp_simple_resource/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@
from pydantic import AnyUrl, FileUrl

SAMPLE_RESOURCES = {
"greeting": "Hello! This is a sample text resource.",
"help": "This server provides a few sample text resources for testing.",
"about": "This is the simple-resource MCP server implementation.",
"greeting": {
"content": "Hello! This is a sample text resource.",
"title": "Welcome Message",
},
"help": {
"content": "This server provides a few sample text resources for testing.",
"title": "Help Documentation",
},
"about": {
"content": "This is the simple-resource MCP server implementation.",
"title": "About This Server",
},
}


Expand All @@ -28,6 +37,7 @@ async def list_resources() -> list[types.Resource]:
types.Resource(
uri=FileUrl(f"file:///{name}.txt"),
name=name,
title=SAMPLE_RESOURCES[name]["title"],
description=f"A sample text resource named {name}",
mimeType="text/plain",
)
Expand All @@ -43,7 +53,7 @@ async def read_resource(uri: AnyUrl) -> str | bytes:
if name not in SAMPLE_RESOURCES:
raise ValueError(f"Unknown resource: {uri}")

return SAMPLE_RESOURCES[name]
return SAMPLE_RESOURCES[name]["content"]

if transport == "sse":
from mcp.server.sse import SseServerTransport
Expand Down
1 change: 1 addition & 0 deletions examples/servers/simple-tool/mcp_simple_tool/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="fetch",
title="Website Fetcher",
description="Fetches a website and returns its content",
inputSchema={
"type": "object",
Expand Down
3 changes: 3 additions & 0 deletions src/mcp/server/fastmcp/prompts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -67,6 +68,7 @@ def from_function(
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.
Expand Down Expand Up @@ -103,6 +105,7 @@ def from_function(

return cls(
name=func_name,
title=title,
description=description or fn.__doc__ or "",
arguments=arguments,
fn=fn,
Expand Down
1 change: 1 addition & 0 deletions src/mcp/server/fastmcp/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/server/fastmcp/resources/resource_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def add_template(
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> ResourceTemplate:
Expand All @@ -59,6 +60,7 @@ def add_template(
fn,
uri_template=uri_template,
name=name,
title=title,
description=description,
mime_type=mime_type,
)
Expand Down
4 changes: 4 additions & 0 deletions src/mcp/server/fastmcp/resources/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -28,6 +29,7 @@ def from_function(
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> ResourceTemplate:
Expand All @@ -45,6 +47,7 @@ def from_function(
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,
Expand All @@ -71,6 +74,7 @@ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
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
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/server/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def from_function(
fn: Callable[..., Any],
uri: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
) -> "FunctionResource":
Expand All @@ -86,6 +87,7 @@ def from_function(
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,
Expand Down
Loading
Loading
0