Add meta parameter support to tools, resources, templates, and prompts decorators (#1294)

This commit is contained in:
Jeremiah Lowin 2025-07-29 14:16:05 -07:00 committed by GitHub
commit 3fc2510110
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 192 additions and 2 deletions

View file

@ -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(
<ParamField body="enabled" type="bool" default="True">
A boolean to enable or disable the prompt. See [Disabling Prompts](#disabling-prompts) for more information
</ParamField>
<ParamField body="meta" type="dict[str, Any] | None">
<VersionBadge version="2.11.0" />
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.
</ParamField>
</Card>
### Argument Types

View file

@ -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:
</ParamField>
</Expandable>
</ParamField>
<ParamField body="meta" type="dict[str, Any] | None">
<VersionBadge version="2.11.0" />
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.
</ParamField>
</Card>
### Return Values

View file

@ -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
</ParamField>
</Expandable>
</ParamField>
<ParamField body="meta" type="dict[str, Any] | None">
<VersionBadge version="2.11.0" />
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.
</ParamField>
</Card>

View file

@ -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]:

View file

@ -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:

View file

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

View file

@ -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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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