8000 Add title to tools, resources, prompts (#972) · error05/python-sdk@a2f8766 · GitHub
[go: up one dir, main page]

Skip to content

Commit a2f8766

Browse files
Add title to tools, resources, prompts (modelcontextprotocol#972)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 029f434 commit a2f8766

File tree

18 files changed

+481
-46
lines changed
  • examples
    • clients/simple-chatbot/mcp_simple_chatbot
    • < 10000 div class="PRIVATE_TreeView-item-visual prc-TreeView-TreeViewItemVisual-dRlGq" aria-hidden="true">
      servers
  • src/mcp
  • tests/server/fastmcp
  • 18 files changed

    +481
    -46
    lines changed

    README.md

    Lines changed: 42 additions & 6 deletions
    Original file line numberDiff line numberDiff line change
    @@ -212,13 +212,13 @@ from mcp.server.fastmcp import FastMCP
    212212
    mcp = FastMCP("My App")
    213213

    214214

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

    220220

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

    237237

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

    243243

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

    262262

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

    267267

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

    921+
    ### Client Display Utilities
    922+
    923+
    When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:
    924+
    925+
    ```python
    926+
    from mcp.shared.metadata_utils import get_display_name
    927+
    from mcp.client.session import ClientSession
    928+
    929+
    930+
    async def display_tools(session: ClientSession):
    931+
    """Display available tools with human-readable names"""
    932+
    tools_response = await session.list_tools()
    933+
    934+
    for tool in tools_response.tools:
    935+
    # get_display_name() returns the title if available, otherwise the name
    936+
    display_name = get_display_name(tool)
    937+
    print(f"Tool: {display_name}")
    938+
    if tool.description:
    939+
    print(f" {tool.description}")
    940+
    941+
    942+
    async def display_resources(session: ClientSession):
    943+
    """Display available resources with human-readable names"""
    944+
    resources_response = await session.list_resources()
    945+
    946+
    for resource in resources_response.resources:
    947+
    display_name = get_display_name(resource)
    948+
    print(f"Resource: {display_name} ({resource.uri})")
    949+
    ```
    950+
    951+
    The `get_display_name()` function implements the proper precedence rules for displaying names:
    952+
    - For tools: `title` > `annotations.title` > `name`
    953+
    - For other objects: `title` > `name`
    954+
    955+
    This ensures your client UI shows the most user-friendly names that servers provide.
    956+
    921957
    ### OAuth Authentication for Clients
    922958

    923959
    The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:

    examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

    Lines changed: 17 additions & 5 deletions
    Original file line numberDiff line numberDiff line change
    @@ -123,7 +123,7 @@ async def list_tools(self) -> list[Any]:
    123123
    for item in tools_response:
    124124
    if isinstance(item, tuple) and item[0] == "tools":
    125125
    tools.extend(
    126-
    Tool(tool.name, tool.description, tool.inputSchema)
    126+
    Tool(tool.name, tool.description, tool.inputSchema, tool.title)
    127127
    for tool in item[1]
    128128
    )
    129129

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

    191191
    def __init__(
    192-
    self, name: str, description: str, input_schema: dict[str, Any]
    192+
    self,
    193+
    name: str,
    194+
    description: str,
    195+
    input_schema: dict[str, Any],
    196+
    title: str | None = None,
    193197
    ) -> None:
    194198
    self.name: str = name
    199+
    self.title: str | None = title
    195200
    self.description: str = description
    196201
    self.input_schema: dict[str, Any] = input_schema
    197202

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

    214-
    return f"""
    215-
    Tool: {self.name}
    216-
    Description: {self.description}
    219+
    # Build the formatted output with title as a separate field
    220+
    output = f"Tool: {self.name}\n"
    221+
    222+
    # Add human-readable title if available
    223+
    if self.title:
    224+
    output += f"User-readable title: {self.title}\n"
    225+
    226+
    output += f"""Description: {self.description}
    217227
    Arguments:
    218228
    {chr(10).join(args_desc)}
    219229
    """
    220230

    231+
    return output
    232+
    221233

    222234
    class LLMClient:
    223235
    """Manages communication with the LLM provider."""

    examples/servers/simple-prompt/mcp_simple_prompt/server.py

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -53,6 +53,7 @@ async def list_prompts() -> list[types.Prompt]:
    5353
    return [
    5454
    types.Prompt(
    5555
    name="simple",
    56+
    title="Simple Assistant Prompt",
    5657
    description="A simple prompt that can take optional context and topic "
    5758
    "arguments",
    5859
    arguments=[

    examples/servers/simple-resource/mcp_simple_resource/server.py

    Lines changed: 14 additions & 4 deletions
    Original file line numberDiff line numberDiff line change
    @@ -5,9 +5,18 @@
    55
    from pydantic import AnyUrl, FileUrl
    66

    77
    SAMPLE_RESOURCES = {
    8-
    "greeting": "Hello! This is a sample text resource.",
    9-
    "help": "This server provides a few sample text resources for testing.",
    10-
    "about": "This is the simple-resource MCP server implementation.",
    8+
    "greeting": {
    9+
    "content": "Hello! This is a sample text resource.",
    10+
    "title": "Welcome Message",
    11+
    },
    12+
    "help": {
    13+
    "content": "This server provides a few sample text resources for testing.",
    14+
    "title": "Help Documentation",
    15+
    },
    16+
    "about": {
    17+
    "content": "This is the simple-resource MCP server implementation.",
    18+
    "title": "About This Server",
    19+
    },
    1120
    }
    1221

    1322

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

    46-
    return SAMPLE_RESOURCES[name]
    56+
    return SAMPLE_RESOURCES[name]["content"]
    4757

    4858
    if transport == "sse":
    4959
    from mcp.server.sse import SseServerTransport

    examples/servers/simple-tool/mcp_simple_tool/server.py

    Lines changed: 1 addition & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -41,6 +41,7 @@ async def list_tools() -> list[types.Tool]:
    4141
    return [
    4242
    types.Tool(
    4343
    name="fetch",
    44+
    title="Website Fetcher",
    4445
    description="Fetches a website and returns its content",
    4546
    inputSchema={
    4647
    "type": "object",

    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,

    0 commit comments

    Comments
     (0)
    0