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