From 3fc25101106e3049ec0be2e9f5c127ea07701d8d Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Tue, 29 Jul 2025 14:16:05 -0700
Subject: [PATCH] Add meta parameter support to tools, resources, templates,
and prompts decorators (#1294)
---
docs/servers/prompts.mdx | 9 ++-
docs/servers/resources.mdx | 9 ++-
docs/servers/tools.mdx | 7 +++
src/fastmcp/prompts/prompt.py | 4 ++
src/fastmcp/resources/resource.py | 4 ++
src/fastmcp/resources/template.py | 4 ++
src/fastmcp/server/server.py | 16 +++++
src/fastmcp/tools/tool.py | 4 ++
tests/prompts/test_prompt.py | 15 +++++
.../resources/test_resource_template_meta.py | 25 ++++++++
tests/resources/test_resources.py | 20 ++++++
tests/server/test_server.py | 61 +++++++++++++++++++
tests/tools/test_tool.py | 16 +++++
13 files changed, 192 insertions(+), 2 deletions(-)
create mode 100644 tests/resources/test_resource_template_meta.py
diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx
index 470bf2e68..14f155da3 100644
--- a/docs/servers/prompts.mdx
+++ b/docs/servers/prompts.mdx
@@ -65,7 +65,8 @@ While FastMCP infers the name and description from your function, you can overri
@mcp.prompt(
name="analyze_data_request", # Custom prompt name
description="Creates a request to analyze data with specific parameters", # Custom description
- tags={"analysis", "data"} # Optional categorization tags
+ tags={"analysis", "data"}, # Optional categorization tags
+ meta={"version": "1.1", "author": "data-team"} # Custom metadata
)
def data_analysis_prompt(
data_uri: str = Field(description="The URI of the resource containing the data."),
@@ -91,6 +92,12 @@ def data_analysis_prompt(
A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information
+
+
+
+
+ Optional meta information about the prompt. This data is passed through to the MCP client as the `_meta` field of the client-side prompt object and can be used for custom metadata, versioning, or other application-specific purposes.
+
### Argument Types
diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx
index 596ae027e..a7583e6a6 100644
--- a/docs/servers/resources.mdx
+++ b/docs/servers/resources.mdx
@@ -73,7 +73,8 @@ mcp = FastMCP(name="DataServer")
name="ApplicationStatus", # Custom name
description="Provides the current status of the application.", # Custom description
mime_type="application/json", # Explicit MIME type
- tags={"monitoring", "status"} # Categorization tags
+ tags={"monitoring", "status"}, # Categorization tags
+ meta={"version": "2.1", "team": "infrastructure"} # Custom metadata
)
def get_application_status() -> dict:
"""Internal function description (ignored if description is provided above)."""
@@ -116,6 +117,12 @@ def get_application_status() -> dict:
+
+
+
+
+ Optional meta information about the resource. This data is passed through to the MCP client as the `_meta` field of the client-side resource object and can be used for custom metadata, versioning, or other application-specific purposes.
+
### Return Values
diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx
index c94f7ea77..8da84c202 100644
--- a/docs/servers/tools.mdx
+++ b/docs/servers/tools.mdx
@@ -58,6 +58,7 @@ While FastMCP infers the name and description from your function, you can overri
name="find_products", # Custom tool name for the LLM
description="Search the product catalog with optional category filtering.", # Custom description
tags={"catalog", "search"}, # Optional tags for organization/filtering
+ meta={"version": "1.2", "author": "product-team"} # Custom metadata
)
def search_products_implementation(query: str, category: str | None = None) -> list[dict]:
"""Internal function description (ignored if description is provided above)."""
@@ -107,6 +108,12 @@ def search_products_implementation(query: str, category: str | None = None) -> l
+
+
+
+
+ Optional meta information about the tool. This data is passed through to the MCP client as the `_meta` field of the client-side tool object and can be used for custom metadata, versioning, or other application-specific purposes.
+
diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py
index 511f5b13d..1790d4a01 100644
--- a/src/fastmcp/prompts/prompt.py
+++ b/src/fastmcp/prompts/prompt.py
@@ -117,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionPrompt:
"""Create a Prompt from a function.
@@ -133,6 +134,7 @@ class Prompt(FastMCPComponent, ABC):
description=description,
tags=tags,
enabled=enabled,
+ meta=meta,
)
@abstractmethod
@@ -158,6 +160,7 @@ class FunctionPrompt(Prompt):
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionPrompt:
"""Create a Prompt from a function.
@@ -252,6 +255,7 @@ class FunctionPrompt(Prompt):
tags=tags or set(),
enabled=enabled if enabled is not None else True,
fn=fn,
+ meta=meta,
)
def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py
index 9f8e1a5b6..eee7b1666 100644
--- a/src/fastmcp/resources/resource.py
+++ b/src/fastmcp/resources/resource.py
@@ -76,6 +76,7 @@ class Resource(FastMCPComponent, abc.ABC):
tags: set[str] | None = None,
enabled: bool | None = None,
annotations: Annotations | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionResource:
return FunctionResource.from_function(
fn=fn,
@@ -87,6 +88,7 @@ class Resource(FastMCPComponent, abc.ABC):
tags=tags,
enabled=enabled,
annotations=annotations,
+ meta=meta,
)
@field_validator("mime_type", mode="before")
@@ -172,6 +174,7 @@ class FunctionResource(Resource):
tags: set[str] | None = None,
enabled: bool | None = None,
annotations: Annotations | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionResource:
"""Create a FunctionResource from a function."""
if isinstance(uri, str):
@@ -186,6 +189,7 @@ class FunctionResource(Resource):
tags=tags or set(),
enabled=enabled if enabled is not None else True,
annotations=annotations,
+ meta=meta,
)
async def read(self) -> str | bytes:
diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py
index 9af3487d2..3bc8701eb 100644
--- a/src/fastmcp/resources/template.py
+++ b/src/fastmcp/resources/template.py
@@ -96,6 +96,7 @@ class ResourceTemplate(FastMCPComponent):
tags: set[str] | None = None,
enabled: bool | None = None,
annotations: Annotations | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionResourceTemplate:
return FunctionResourceTemplate.from_function(
fn=fn,
@@ -107,6 +108,7 @@ class ResourceTemplate(FastMCPComponent):
tags=tags,
enabled=enabled,
annotations=annotations,
+ meta=meta,
)
@field_validator("mime_type", mode="before")
@@ -219,6 +221,7 @@ class FunctionResourceTemplate(ResourceTemplate):
tags: set[str] | None = None,
enabled: bool | None = None,
annotations: Annotations | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionResourceTemplate:
"""Create a template from a function."""
from fastmcp.server.context import Context
@@ -304,4 +307,5 @@ class FunctionResourceTemplate(ResourceTemplate):
tags=tags or set(),
enabled=enabled if enabled is not None else True,
annotations=annotations,
+ meta=meta,
)
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 4b7cd056c..6673818a6 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -871,6 +871,7 @@ class FastMCP(Generic[LifespanResultT]):
output_schema: dict[str, Any] | None | NotSetT = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
+ meta: dict[str, Any] | None = None,
enabled: bool | None = None,
) -> FunctionTool: ...
@@ -886,6 +887,7 @@ class FastMCP(Generic[LifespanResultT]):
output_schema: dict[str, Any] | None | NotSetT = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
+ meta: dict[str, Any] | None = None,
enabled: bool | None = None,
) -> Callable[[AnyFunction], FunctionTool]: ...
@@ -900,6 +902,7 @@ class FastMCP(Generic[LifespanResultT]):
output_schema: dict[str, Any] | None | NotSetT = NotSet,
annotations: ToolAnnotations | dict[str, Any] | None = None,
exclude_args: list[str] | None = None,
+ meta: dict[str, Any] | None = None,
enabled: bool | None = None,
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
"""Decorator to register a tool.
@@ -923,6 +926,7 @@ class FastMCP(Generic[LifespanResultT]):
output_schema: Optional JSON schema for the tool's output
annotations: Optional annotations about the tool's behavior
exclude_args: Optional list of argument names to exclude from the tool schema
+ meta: Optional meta information about the tool
enabled: Optional boolean to enable or disable the tool
Examples:
@@ -981,6 +985,7 @@ class FastMCP(Generic[LifespanResultT]):
output_schema=output_schema,
annotations=annotations,
exclude_args=exclude_args,
+ meta=meta,
serializer=self._tool_serializer,
enabled=enabled,
)
@@ -1013,6 +1018,7 @@ class FastMCP(Generic[LifespanResultT]):
output_schema=output_schema,
annotations=annotations,
exclude_args=exclude_args,
+ meta=meta,
enabled=enabled,
)
@@ -1111,6 +1117,7 @@ class FastMCP(Generic[LifespanResultT]):
tags: set[str] | None = None,
enabled: bool | None = None,
annotations: Annotations | dict[str, Any] | None = None,
+ meta: dict[str, Any] | None = None,
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
"""Decorator to register a function as a resource.
@@ -1135,6 +1142,7 @@ class FastMCP(Generic[LifespanResultT]):
tags: Optional set of tags for categorizing the resource
enabled: Optional boolean to enable or disable the resource
annotations: Optional annotations about the resource's behavior
+ meta: Optional meta information about the resource
Examples:
Register a resource with a custom name:
@@ -1208,6 +1216,7 @@ class FastMCP(Generic[LifespanResultT]):
tags=tags,
enabled=enabled,
annotations=annotations,
+ meta=meta,
)
self.add_template(template)
return template
@@ -1222,6 +1231,7 @@ class FastMCP(Generic[LifespanResultT]):
tags=tags,
enabled=enabled,
annotations=annotations,
+ meta=meta,
)
self.add_resource(resource)
return resource
@@ -1266,6 +1276,7 @@ class FastMCP(Generic[LifespanResultT]):
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
+ meta: dict[str, Any] | None = None,
) -> FunctionPrompt: ...
@overload
@@ -1278,6 +1289,7 @@ class FastMCP(Generic[LifespanResultT]):
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
+ meta: dict[str, Any] | None = None,
) -> Callable[[AnyFunction], FunctionPrompt]: ...
def prompt(
@@ -1289,6 +1301,7 @@ class FastMCP(Generic[LifespanResultT]):
description: str | None = None,
tags: set[str] | None = None,
enabled: bool | None = None,
+ meta: dict[str, Any] | None = None,
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
"""Decorator to register a prompt.
@@ -1309,6 +1322,7 @@ class FastMCP(Generic[LifespanResultT]):
description: Optional description of what the prompt does
tags: Optional set of tags for categorizing the prompt
enabled: Optional boolean to enable or disable the prompt
+ meta: Optional meta information about the prompt
Examples:
@@ -1386,6 +1400,7 @@ class FastMCP(Generic[LifespanResultT]):
description=description,
tags=tags,
enabled=enabled,
+ meta=meta,
)
self.add_prompt(prompt)
@@ -1415,6 +1430,7 @@ class FastMCP(Generic[LifespanResultT]):
description=description,
tags=tags,
enabled=enabled,
+ meta=meta,
)
async def run_stdio_async(self, show_banner: bool = True) -> None:
diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py
index 5030822f3..0377027c6 100644
--- a/src/fastmcp/tools/tool.py
+++ b/src/fastmcp/tools/tool.py
@@ -165,6 +165,7 @@ class Tool(FastMCPComponent):
exclude_args: list[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
serializer: Callable[[Any], str] | None = None,
+ meta: dict[str, Any] | None = None,
enabled: bool | None = None,
) -> FunctionTool:
"""Create a Tool from a function."""
@@ -178,6 +179,7 @@ class Tool(FastMCPComponent):
exclude_args=exclude_args,
output_schema=output_schema,
serializer=serializer,
+ meta=meta,
enabled=enabled,
)
@@ -240,6 +242,7 @@ class FunctionTool(Tool):
exclude_args: list[str] | None = None,
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
serializer: Callable[[Any], str] | None = None,
+ meta: dict[str, Any] | None = None,
enabled: bool | None = None,
) -> FunctionTool:
"""Create a Tool from a function."""
@@ -272,6 +275,7 @@ class FunctionTool(Tool):
annotations=annotations,
tags=tags or set(),
serializer=serializer,
+ meta=meta,
enabled=enabled if enabled is not None else True,
)
diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py
index be5d0a1f3..76132b902 100644
--- a/tests/prompts/test_prompt.py
+++ b/tests/prompts/test_prompt.py
@@ -482,3 +482,18 @@ class TestPromptArgumentDescriptions:
"Provide as a JSON string matching the following schema:"
not in arg.description
)
+
+ def test_prompt_meta_parameter(self):
+ """Test that meta parameter is properly handled."""
+
+ def test_prompt(message: str) -> str:
+ return f"Response: {message}"
+
+ meta_data = {"version": "3.0", "type": "prompt"}
+ prompt = Prompt.from_function(test_prompt, meta=meta_data)
+
+ assert prompt.meta == meta_data
+ mcp_prompt = prompt.to_mcp_prompt()
+ # MCP prompt includes fastmcp meta, so check that our meta is included
+ assert mcp_prompt.meta is not None
+ assert meta_data.items() <= mcp_prompt.meta.items()
diff --git a/tests/resources/test_resource_template_meta.py b/tests/resources/test_resource_template_meta.py
new file mode 100644
index 000000000..b08ef3667
--- /dev/null
+++ b/tests/resources/test_resource_template_meta.py
@@ -0,0 +1,25 @@
+from fastmcp.resources import ResourceTemplate
+
+
+class TestResourceTemplateMeta:
+ """Test ResourceTemplate meta functionality."""
+
+ def test_template_meta_parameter(self):
+ """Test that meta parameter is properly handled."""
+
+ def template_func(param: str) -> str:
+ return f"Result: {param}"
+
+ meta_data = {"version": "2.0", "template": "test"}
+ template = ResourceTemplate.from_function(
+ fn=template_func,
+ uri_template="test://{param}",
+ name="test_template",
+ meta=meta_data,
+ )
+
+ assert template.meta == meta_data
+ mcp_template = template.to_mcp_template()
+ # MCP template includes fastmcp meta, so check that our meta is included
+ assert mcp_template.meta is not None
+ assert meta_data.items() <= mcp_template.meta.items()
diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py
index 33e76d71e..1e165eea9 100644
--- a/tests/resources/test_resources.py
+++ b/tests/resources/test_resources.py
@@ -93,3 +93,23 @@ class TestResourceValidation:
with pytest.raises(TypeError, match="abstract method"):
ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore
+
+ def test_resource_meta_parameter(self):
+ """Test that meta parameter is properly handled."""
+
+ def resource_func() -> str:
+ return "test content"
+
+ meta_data = {"version": "1.0", "category": "test"}
+ resource = Resource.from_function(
+ fn=resource_func,
+ uri="resource://test",
+ name="test_resource",
+ meta=meta_data,
+ )
+
+ assert resource.meta == meta_data
+ mcp_resource = resource.to_mcp_resource()
+ # MCP resource includes fastmcp meta, so check that our meta is included
+ assert mcp_resource.meta is not None
+ assert meta_data.items() <= mcp_resource.meta.items()
diff --git a/tests/server/test_server.py b/tests/server/test_server.py
index 0bf13df0c..827d3190d 100644
--- a/tests/server/test_server.py
+++ b/tests/server/test_server.py
@@ -421,6 +421,22 @@ class TestToolDecorator:
def my_function(x: int) -> str:
return f"Result: {x}"
+ async def test_tool_decorator_with_meta(self):
+ """Test that meta parameter is passed through the tool decorator."""
+ mcp = FastMCP()
+
+ meta_data = {"version": "1.0", "author": "test"}
+
+ @mcp.tool(meta=meta_data)
+ def multiply(a: int, b: int) -> int:
+ """Multiply two numbers."""
+ return a * b
+
+ tools_dict = await mcp.get_tools()
+ tool = tools_dict["multiply"]
+
+ assert tool.meta == meta_data
+
class TestResourceDecorator:
async def test_no_resources_before_decorator(self):
@@ -584,6 +600,21 @@ class TestResourceDecorator:
result = await client.read_resource("resource://data")
assert result[0].text == "Static Hello, world!" # type: ignore[attr-defined]
+ async def test_resource_decorator_with_meta(self):
+ """Test that meta parameter is passed through the resource decorator."""
+ mcp = FastMCP()
+
+ meta_data = {"version": "1.0", "author": "test"}
+
+ @mcp.resource("resource://data", meta=meta_data)
+ def get_data() -> str:
+ return "Hello, world!"
+
+ resources_dict = await mcp.get_resources()
+ resource = resources_dict["resource://data"]
+
+ assert resource.meta == meta_data
+
class TestTemplateDecorator:
async def test_template_decorator(self):
@@ -733,6 +764,21 @@ class TestTemplateDecorator:
assert template.uri_template == "resource://{param*}"
assert template.name == "template_resource"
+ async def test_template_decorator_with_meta(self):
+ """Test that meta parameter is passed through the template decorator."""
+ mcp = FastMCP()
+
+ meta_data = {"version": "2.0", "template": "test"}
+
+ @mcp.resource("resource://{param}/data", meta=meta_data)
+ def get_template_data(param: str) -> str:
+ return f"Data for {param}"
+
+ templates_dict = await mcp.get_resource_templates()
+ template = templates_dict["resource://{param}/data"]
+
+ assert template.meta == meta_data
+
class TestPromptDecorator:
async def test_prompt_decorator(self):
@@ -988,6 +1034,21 @@ class TestPromptDecorator:
message = result.messages[0]
assert message.content.text == "Static Hello, world!" # type: ignore[attr-defined]
+ async def test_prompt_decorator_with_meta(self):
+ """Test that meta parameter is passed through the prompt decorator."""
+ mcp = FastMCP()
+
+ meta_data = {"version": "3.0", "type": "prompt"}
+
+ @mcp.prompt(meta=meta_data)
+ def test_prompt(message: str) -> str:
+ return f"Response: {message}"
+
+ prompts_dict = await mcp.get_prompts()
+ prompt = prompts_dict["test_prompt"]
+
+ assert prompt.meta == meta_data
+
class TestResourcePrefixHelpers:
@pytest.mark.parametrize(
diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py
index de006ae34..03ec77331 100644
--- a/tests/tools/test_tool.py
+++ b/tests/tools/test_tool.py
@@ -44,6 +44,22 @@ class TestToolFromFunction:
}
assert tool.output_schema == expected_schema
+ def test_meta_parameter(self):
+ """Test that meta parameter is properly handled."""
+
+ def multiply(a: int, b: int) -> int:
+ """Multiply two numbers."""
+ return a * b
+
+ meta_data = {"version": "1.0", "author": "test"}
+ tool = Tool.from_function(multiply, meta=meta_data)
+
+ assert tool.meta == meta_data
+ mcp_tool = tool.to_mcp_tool()
+ # MCP tool includes fastmcp meta, so check that our meta is included
+ assert mcp_tool.meta is not None
+ assert meta_data.items() <= mcp_tool.meta.items()
+
async def test_async_function(self):
"""Test registering and running an async function."""