diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index d27892513..98518a24b 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -99,6 +99,7 @@ class Prompt(FastMCPComponent, ABC): "name": self.name, "description": self.description, "arguments": arguments, + "title": self.title, } return MCPPrompt(**kwargs | overrides) @@ -106,6 +107,7 @@ class Prompt(FastMCPComponent, ABC): 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 +121,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 +148,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 +240,7 @@ class FunctionPrompt(Prompt): return cls( name=func_name, + title=title, description=description, arguments=arguments, tags=tags or set(), diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index dd04f8a3f..77941e73d 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -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, @@ -111,6 +113,7 @@ class Resource(FastMCPComponent, abc.ABC): "name": self.name, "description": self.description, "mimeType": self.mime_type, + "title": self.title, } return MCPResource(**kwargs | overrides) @@ -149,6 +152,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 +165,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(), diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py index 00fc29863..6680de9c9 100644 --- a/src/fastmcp/resources/template.py +++ b/src/fastmcp/resources/template.py @@ -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, @@ -144,6 +146,7 @@ class ResourceTemplate(FastMCPComponent): "name": self.name, "description": self.description, "mimeType": self.mime_type, + "title": self.title, } return MCPResourceTemplate(**kwargs | overrides) @@ -197,6 +200,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 +282,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, diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index d3252b771..c424fa6e1 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -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, diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 35dec76f6..03cab7d3c 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -135,12 +135,20 @@ class Tool(FastMCPComponent): pass # No context available def to_mcp_tool(self, **overrides: Any) -> MCPTool: + if self.title: + title = self.title + elif self.annotations and self.annotations.title: + title = self.annotations.title + else: + title = None + kwargs = { "name": self.name, "description": self.description, "inputSchema": self.parameters, "outputSchema": self.output_schema, "annotations": self.annotations, + "title": title, } return MCPTool(**kwargs | overrides) @@ -148,6 +156,7 @@ class Tool(FastMCPComponent): 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 +169,7 @@ class Tool(FastMCPComponent): return FunctionTool.from_function( fn=fn, name=name, + title=title, description=description, tags=tags, annotations=annotations, @@ -219,6 +229,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 +261,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, diff --git a/src/fastmcp/utilities/components.py b/src/fastmcp/utilities/components.py index 2c3bbb2e9..6e7a36418 100644 --- a/src/fastmcp/utilities/components.py +++ b/src/fastmcp/utilities/components.py @@ -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 diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index f94e73fd3..a2278d6d1 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -1225,3 +1225,97 @@ 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" + + # 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 + + # 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 + + def test_tool_title_priority(self): + """Test that explicit title takes priority over annotations.title.""" + from mcp.types import ToolAnnotations + + def divide(x: int, y: int) -> float: + """Divide two numbers.""" + return x / y + + # Test with both explicit title and annotations.title + annotations = ToolAnnotations(title="Annotation Title") + tool = Tool.from_function( + divide, + name="div", + title="Explicit Title", + annotations=annotations, + ) + + assert tool.title == "Explicit Title" + assert tool.annotations is not None + assert tool.annotations.title == "Annotation Title" + + # Explicit title should take priority + mcp_tool = tool.to_mcp_tool() + assert mcp_tool.title == "Explicit Title" + + def test_tool_annotations_title_fallback(self): + """Test that annotations.title is used when no explicit title is provided.""" + from mcp.types import ToolAnnotations + + def modulo(x: int, y: int) -> int: + """Get modulo of two numbers.""" + return x % y + + # Test with only annotations.title (no explicit title) + annotations = ToolAnnotations(title="Annotation Title") + tool = Tool.from_function( + modulo, + name="mod", + annotations=annotations, + ) + + assert tool.title is None + assert tool.annotations is not None + assert tool.annotations.title == "Annotation Title" + + # Should fall back to annotations.title + mcp_tool = tool.to_mcp_tool() + assert mcp_tool.title == "Annotation Title"