mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
feat: Add title, annotations and meta to mixin decorators
This commit is contained in:
parent
aecf7f07c2
commit
26e2414b48
4 changed files with 118 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue