diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index c57ed571b..24ebcfa29 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -81,6 +81,10 @@ def data_analysis_prompt( Sets the explicit prompt name exposed via MCP. If not provided, uses the function name + + A human-readable title for the prompt + + Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose @@ -340,4 +344,4 @@ The duplicate behavior options are: - `"warn"` (default): Logs a warning, and the new prompt replaces the old one. - `"error"`: Raises a `ValueError`, preventing the duplicate registration. - `"replace"`: Silently replaces the existing prompt with the new one. -- `"ignore"`: Keeps the original prompt and ignores the new registration attempt. \ No newline at end of file +- `"ignore"`: Keeps the original prompt and ignores the new registration attempt. diff --git a/src/fastmcp/contrib/mcp_mixin/README.md b/src/fastmcp/contrib/mcp_mixin/README.md index 0742d7b6a..39c3a2352 100644 --- a/src/fastmcp/contrib/mcp_mixin/README.md +++ b/src/fastmcp/contrib/mcp_mixin/README.md @@ -11,12 +11,15 @@ Tools: * [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools) * [annotations](https://gofastmcp.com/servers/tools#annotations-2) * [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments) +* [meta](https://gofastmcp.com/servers/tools#param-meta) Prompts: * [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts) +* [meta](https://gofastmcp.com/servers/prompts#param-meta) Resources: * [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources) +* [meta](https://gofastmcp.com/servers/resources#param-meta) ## Usage @@ -78,7 +81,16 @@ class MyComponent(MCPMixin): if delete_all: return "99 records deleted. I bet you're not a tool :)" return "Tool executed, but you might be a tool!" - + + # example tool w/ meta + @mcp_tool( + name="data_tool", + description="Fetches user data from database", + meta={"version": "2.0", "category": "database", "author": "dev-team"} + ) + def data_tool_method(self, user_id: int): + return f"Fetching data for user {user_id}" + @mcp_resource(uri="component://data") def resource_method(self): return {"data": "some data"} @@ -88,6 +100,15 @@ class MyComponent(MCPMixin): def resource_method(self): return {"data": "some data"} + # example resource w/meta and title + @mcp_resource( + uri="component://config", + title="Data resource Title, + meta={"internal": True, "cache_ttl": 3600, "priority": "high"} + ) + def config_resource_method(self): + return {"config": "data"} + # prompt @mcp_prompt(name="A prompt") def prompt_method(self, name): @@ -98,6 +119,16 @@ class MyComponent(MCPMixin): def prompt_method(self, name): return f"What's up {name}?" + # example prompt w/title and meta + @mcp_prompt( + name="analysis_prompt", + title="Data Analysis Prompt", + description="Analyzes data patterns", + meta={"complexity": "high", "domain": "analytics", "requires_context": True} + ) + def analysis_prompt_method(self, dataset: str): + return f"Analyze the patterns in {dataset}" + mcp_server = FastMCP() component = MyComponent() diff --git a/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py b/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py index 8e11e6342..5688fa125 100644 --- a/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +++ b/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py @@ -3,7 +3,7 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Any -from mcp.types import ToolAnnotations +from mcp.types import Annotations, ToolAnnotations from fastmcp.prompts.prompt import Prompt from fastmcp.resources.resource import Resource @@ -29,6 +29,7 @@ def mcp_tool( annotations: ToolAnnotations | dict[str, Any] | None = None, exclude_args: list[str] | None = None, serializer: Callable[[Any], str] | None = None, + meta: dict[str, Any] | None = None, enabled: bool | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP tool for later registration.""" @@ -41,6 +42,7 @@ def mcp_tool( "annotations": annotations, "exclude_args": exclude_args, "serializer": serializer, + "meta": meta, "enabled": enabled, } call_args = {k: v for k, v in call_args.items() if v is not None} @@ -54,9 +56,12 @@ def mcp_resource( uri: str, *, name: str | None = None, + title: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, + annotations: Annotations | None = None, + meta: dict[str, Any] | None = None, enabled: bool | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP resource for later registration.""" @@ -65,9 +70,12 @@ def mcp_resource( call_args = { "uri": uri, "name": name or get_fn_name(func), + "title": title, "description": description, "mime_type": mime_type, "tags": tags, + "annotations": annotations, + "meta": meta, "enabled": enabled, } call_args = {k: v for k, v in call_args.items() if v is not None} @@ -81,8 +89,10 @@ def mcp_resource( def mcp_prompt( name: str | None = None, + title: str | None = None, description: str | None = None, tags: set[str] | None = None, + meta: dict[str, Any] | None = None, enabled: bool | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP prompt for later registration.""" @@ -90,8 +100,10 @@ def mcp_prompt( def decorator(func: Callable[..., Any]) -> Callable[..., Any]: call_args = { "name": name or get_fn_name(func), + "title": title, "description": description, "tags": tags, + "meta": meta, "enabled": enabled, } @@ -151,7 +163,6 @@ class MCPMixin: tool = Tool.from_function( fn=method, name=registration_info.get("name"), - title=registration_info.get("title"), description=registration_info.get("description"), tags=registration_info.get("tags"), annotations=registration_info.get("annotations"), @@ -195,6 +206,7 @@ class MCPMixin: fn=method, uri=registration_info["uri"], name=registration_info.get("name"), + title=registration_info.get("title"), description=registration_info.get("description"), mime_type=registration_info.get("mime_type"), tags=registration_info.get("tags"), diff --git a/tests/contrib/test_mcp_mixin.py b/tests/contrib/test_mcp_mixin.py index a39b293e3..04ad5eb69 100644 --- a/tests/contrib/test_mcp_mixin.py +++ b/tests/contrib/test_mcp_mixin.py @@ -253,3 +253,70 @@ class TestMCPMixin: assert f"cust{_DEFAULT_SEPARATOR_TOOL}tool_cust" not in tools assert f"cust{_DEFAULT_SEPARATOR_RESOURCE}res://cust" not in resources assert f"cust{_DEFAULT_SEPARATOR_PROMPT}prompt_cust" not in prompts + + async def test_tool_with_title_and_meta(self): + """Test that title (via annotations) and meta arguments are properly passed through.""" + from mcp.types import ToolAnnotations + + mcp = FastMCP() + + class MyToolWithMeta(MCPMixin): + @mcp_tool( + annotations=ToolAnnotations(title="My Tool Title"), + meta={"version": "1.0", "author": "test"}, + ) + def sample_tool(self): + pass + + instance = MyToolWithMeta() + instance.register_tools(mcp) + + registered_tools = await mcp.get_tools() + tool = registered_tools["sample_tool"] + + assert tool.annotations is not None + assert tool.annotations.title == "My Tool Title" + assert tool.meta == {"version": "1.0", "author": "test"} + + async def test_resource_with_meta(self): + """Test that meta argument is properly passed through for resources.""" + mcp = FastMCP() + + class MyResourceWithMeta(MCPMixin): + @mcp_resource( + uri="test://resource", + title="My Resource Title", + meta={"category": "data", "internal": True}, + ) + def sample_resource(self): + pass + + instance = MyResourceWithMeta() + instance.register_resources(mcp) + + registered_resources = await mcp.get_resources() + resource = registered_resources["test://resource"] + + assert resource.meta == {"category": "data", "internal": True} + assert resource.title == "My Resource Title" + + async def test_prompt_with_title_and_meta(self): + """Test that title and meta arguments are properly passed through for prompts.""" + mcp = FastMCP() + + class MyPromptWithMeta(MCPMixin): + @mcp_prompt( + title="My Prompt Title", + meta={"priority": "high", "category": "analysis"}, + ) + def sample_prompt(self): + pass + + instance = MyPromptWithMeta() + instance.register_prompts(mcp) + + prompts = await mcp.get_prompts() + prompt = prompts["sample_prompt"] + + assert prompt.title == "My Prompt Title" + assert prompt.meta == {"priority": "high", "category": "analysis"}