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