feat: Add title, annotations and meta to mixin decorators

This commit is contained in:
Ankesh 2025-10-09 15:36:15 +05:30
commit 26e2414b48
4 changed files with 118 additions and 4 deletions

View file

@ -81,6 +81,10 @@ def data_analysis_prompt(
Sets the explicit prompt name exposed via MCP. If not provided, uses the function name
</ParamField>
<ParamField body="title" type="str | None">
A human-readable title for the prompt
</ParamField>
<ParamField body="description" type="str | None">
Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose
</ParamField>
@ -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.
- `"ignore"`: Keeps the original prompt and ignores the new registration attempt.

View file

@ -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()

View file

@ -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"),

View file

@ -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"}