From 24deabf568dc5066f324151c2a76fa4b9e2b69bf Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sat, 28 Jun 2025 18:11:29 -0400 Subject: [PATCH] Add title field support to FastMCP components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/fastmcp/prompts/prompt.py | 13 +++++++- src/fastmcp/resources/resource.py | 7 +++++ src/fastmcp/resources/template.py | 7 +++++ src/fastmcp/server/server.py | 13 ++++++++ src/fastmcp/tools/tool.py | 7 +++++ src/fastmcp/utilities/components.py | 10 +++++- tests/tools/test_tool.py | 47 +++++++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index d27892513..5022d0711 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -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(), diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index dd04f8a3f..113960630 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, @@ -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(), diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py index 00fc29863..d8558e746 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, @@ -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, 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..c9787adc6 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -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, 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..9350147c1 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -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