Add title field support to FastMCP components

- Add optional title field to FastMCPComponent base class
- Support title parameter in all component decorators (@tool, @resource, @prompt)
- Include title in MCP protocol serialization when provided
- Add get_display_name() method to prefer title over name for display
- Add comprehensive tests for title functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jeremiah Lowin 2025-06-28 18:11:29 -04:00
commit 24deabf568
7 changed files with 102 additions and 2 deletions

View file

@ -100,12 +100,16 @@ class Prompt(FastMCPComponent, ABC):
"description": self.description,
"arguments": arguments,
}
# Add title field if provided
if self.title is not None:
kwargs["title"] = self.title
return MCPPrompt(**kwargs | overrides)
@staticmethod
def from_function(
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@ -119,7 +123,12 @@ class Prompt(FastMCPComponent, ABC):
- A sequence of any of the above
"""
return FunctionPrompt.from_function(
fn=fn, name=name, description=description, tags=tags, enabled=enabled
fn=fn,
name=name,
title=title,
description=description,
tags=tags,
enabled=enabled,
)
@abstractmethod
@ -141,6 +150,7 @@ class FunctionPrompt(Prompt):
cls,
fn: Callable[..., PromptResult | Awaitable[PromptResult]],
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@ -232,6 +242,7 @@ class FunctionPrompt(Prompt):
return cls(
name=func_name,
title=title,
description=description,
arguments=arguments,
tags=tags or set(),

View file

@ -65,6 +65,7 @@ class Resource(FastMCPComponent, abc.ABC):
fn: Callable[[], Any],
uri: str | AnyUrl,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
@ -74,6 +75,7 @@ class Resource(FastMCPComponent, abc.ABC):
fn=fn,
uri=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
tags=tags,
@ -112,6 +114,9 @@ class Resource(FastMCPComponent, abc.ABC):
"description": self.description,
"mimeType": self.mime_type,
}
# Add title field if provided
if self.title is not None:
kwargs["title"] = self.title
return MCPResource(**kwargs | overrides)
def __repr__(self) -> str:
@ -149,6 +154,7 @@ class FunctionResource(Resource):
fn: Callable[[], Any],
uri: str | AnyUrl,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
@ -161,6 +167,7 @@ class FunctionResource(Resource):
fn=fn,
uri=uri,
name=name or fn.__name__,
title=title,
description=description or inspect.getdoc(fn),
mime_type=mime_type or "text/plain",
tags=tags or set(),

View file

@ -86,6 +86,7 @@ class ResourceTemplate(FastMCPComponent):
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
@ -95,6 +96,7 @@ class ResourceTemplate(FastMCPComponent):
fn=fn,
uri_template=uri_template,
name=name,
title=title,
description=description,
mime_type=mime_type,
tags=tags,
@ -145,6 +147,9 @@ class ResourceTemplate(FastMCPComponent):
"description": self.description,
"mimeType": self.mime_type,
}
# Add title field if provided
if self.title is not None:
kwargs["title"] = self.title
return MCPResourceTemplate(**kwargs | overrides)
@classmethod
@ -197,6 +202,7 @@ class FunctionResourceTemplate(ResourceTemplate):
fn: Callable[..., Any],
uri_template: str,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
@ -278,6 +284,7 @@ class FunctionResourceTemplate(ResourceTemplate):
return cls(
uri_template=uri_template,
name=func_name,
title=title,
description=description,
mime_type=mime_type or "text/plain",
fn=fn,

View file

@ -790,6 +790,7 @@ class FastMCP(Generic[LifespanResultT]):
name_or_fn: AnyFunction,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
@ -804,6 +805,7 @@ class FastMCP(Generic[LifespanResultT]):
name_or_fn: str | None = None,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
@ -817,6 +819,7 @@ class FastMCP(Generic[LifespanResultT]):
name_or_fn: str | AnyFunction | None = None,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT = NotSet,
@ -897,6 +900,7 @@ class FastMCP(Generic[LifespanResultT]):
tool = Tool.from_function(
fn,
name=tool_name,
title=title,
description=description,
tags=tags,
output_schema=output_schema,
@ -928,6 +932,7 @@ class FastMCP(Generic[LifespanResultT]):
return partial(
self.tool,
name=tool_name,
title=title,
description=description,
tags=tags,
output_schema=output_schema,
@ -1016,6 +1021,7 @@ class FastMCP(Generic[LifespanResultT]):
uri: str,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
mime_type: str | None = None,
tags: set[str] | None = None,
@ -1107,6 +1113,7 @@ class FastMCP(Generic[LifespanResultT]):
fn=fn,
uri_template=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
tags=tags,
@ -1119,6 +1126,7 @@ class FastMCP(Generic[LifespanResultT]):
fn=fn,
uri=uri,
name=name,
title=title,
description=description,
mime_type=mime_type,
tags=tags,
@ -1158,6 +1166,7 @@ class FastMCP(Generic[LifespanResultT]):
name_or_fn: AnyFunction,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@ -1169,6 +1178,7 @@ class FastMCP(Generic[LifespanResultT]):
name_or_fn: str | None = None,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@ -1179,6 +1189,7 @@ class FastMCP(Generic[LifespanResultT]):
name_or_fn: str | AnyFunction | None = None,
*,
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
@ -1275,6 +1286,7 @@ class FastMCP(Generic[LifespanResultT]):
prompt = Prompt.from_function(
fn=fn,
name=prompt_name,
title=title,
description=description,
tags=tags,
enabled=enabled,
@ -1303,6 +1315,7 @@ class FastMCP(Generic[LifespanResultT]):
return partial(
self.prompt,
name=prompt_name,
title=title,
description=description,
tags=tags,
enabled=enabled,

View file

@ -142,12 +142,16 @@ class Tool(FastMCPComponent):
"outputSchema": self.output_schema,
"annotations": self.annotations,
}
# Add title field if provided
if self.title is not None:
kwargs["title"] = self.title
return MCPTool(**kwargs | overrides)
@staticmethod
def from_function(
fn: Callable[..., Any],
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | None = None,
@ -160,6 +164,7 @@ class Tool(FastMCPComponent):
return FunctionTool.from_function(
fn=fn,
name=name,
title=title,
description=description,
tags=tags,
annotations=annotations,
@ -219,6 +224,7 @@ class FunctionTool(Tool):
cls,
fn: Callable[..., Any],
name: str | None = None,
title: str | None = None,
description: str | None = None,
tags: set[str] | None = None,
annotations: ToolAnnotations | None = None,
@ -250,6 +256,7 @@ class FunctionTool(Tool):
return cls(
fn=parsed_fn.fn,
name=name or parsed_fn.name,
title=title,
description=description or parsed_fn.description,
parameters=parsed_fn.input_schema,
output_schema=output_schema,

View file

@ -24,6 +24,10 @@ class FastMCPComponent(FastMCPBaseModel):
name: str = Field(
description="The name of the component.",
)
title: str | None = Field(
default=None,
description="The title of the component for display purposes.",
)
description: str | None = Field(
default=None,
description="The description of the component.",
@ -64,7 +68,7 @@ class FastMCPComponent(FastMCPBaseModel):
return self.model_dump() == other.model_dump()
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, description={self.description!r}, tags={self.tags}, enabled={self.enabled})"
return f"{self.__class__.__name__}(name={self.name!r}, title={self.title!r}, description={self.description!r}, tags={self.tags}, enabled={self.enabled})"
def enable(self) -> None:
"""Enable the component."""
@ -73,3 +77,7 @@ class FastMCPComponent(FastMCPBaseModel):
def disable(self) -> None:
"""Disable the component."""
self.enabled = False
def get_display_name(self) -> str:
"""Get the display name for this component, preferring title over name."""
return self.title if self.title is not None else self.name

View file

@ -1225,3 +1225,50 @@ class TestAutomaticStructuredContent:
assert result.data.name == "Bob"
assert result.data.age == 25
assert result.data.verified is True
class TestToolTitle:
"""Tests for tool title functionality."""
def test_tool_with_title(self):
"""Test that tools can have titles and they appear in MCP conversion."""
def calculate(x: int, y: int) -> int:
"""Calculate the sum of two numbers."""
return x + y
tool = Tool.from_function(
calculate,
name="calc",
title="Advanced Calculator Tool",
description="Custom description",
)
assert tool.name == "calc"
assert tool.title == "Advanced Calculator Tool"
assert tool.description == "Custom description"
assert tool.get_display_name() == "Advanced Calculator Tool"
# Test MCP conversion includes title
mcp_tool = tool.to_mcp_tool()
assert mcp_tool.name == "calc"
assert (
hasattr(mcp_tool, "title") and mcp_tool.title == "Advanced Calculator Tool"
)
def test_tool_without_title(self):
"""Test that tools without titles use name as display name."""
def multiply(a: int, b: int) -> int:
return a * b
tool = Tool.from_function(multiply)
assert tool.name == "multiply"
assert tool.title is None
assert tool.get_display_name() == "multiply"
# Test MCP conversion doesn't include title when None
mcp_tool = tool.to_mcp_tool()
assert mcp_tool.name == "multiply"
assert not hasattr(mcp_tool, "title") or mcp_tool.title is None