From a41aba34868d50a5cae216ac9258077e82024e8a Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 28 Jul 2025 17:38:18 -0400 Subject: [PATCH] Add tags to meta --- src/fastmcp/prompts/prompt.py | 1 + src/fastmcp/resources/resource.py | 1 + src/fastmcp/resources/template.py | 1 + src/fastmcp/tools/tool.py | 1 + src/fastmcp/utilities/components.py | 8 ++- tests/server/test_server_interactions.py | 69 ++++++++++++++++++++++++ 6 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 1477fab53..e098c7369 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -100,6 +100,7 @@ class Prompt(FastMCPComponent, ABC): "description": self.description, "arguments": arguments, "title": self.title, + "_meta": self.get_meta(), } return MCPPrompt(**kwargs | overrides) diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index 25132d75b..3527e541e 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -122,6 +122,7 @@ class Resource(FastMCPComponent, abc.ABC): "mimeType": self.mime_type, "title": self.title, "annotations": self.annotations, + "_meta": self.get_meta(), } return MCPResource(**kwargs | overrides) diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py index db7d4a702..6ab9dbb99 100644 --- a/src/fastmcp/resources/template.py +++ b/src/fastmcp/resources/template.py @@ -154,6 +154,7 @@ class ResourceTemplate(FastMCPComponent): "mimeType": self.mime_type, "title": self.title, "annotations": self.annotations, + "_meta": self.get_meta(), } return MCPResourceTemplate(**kwargs | overrides) diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 0cb31523f..a9d95ecd6 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -145,6 +145,7 @@ class Tool(FastMCPComponent): "outputSchema": self.output_schema, "annotations": self.annotations, "title": title, + "_meta": self.get_meta(), } return MCPTool(**kwargs | overrides) diff --git a/src/fastmcp/utilities/components.py b/src/fastmcp/utilities/components.py index 41932cb7a..a831f60de 100644 --- a/src/fastmcp/utilities/components.py +++ b/src/fastmcp/utilities/components.py @@ -36,7 +36,9 @@ class FastMCPComponent(FastMCPBaseModel): default_factory=set, description="Tags for the component.", ) - + meta: dict[str, Any] = Field( + default_factory=dict, description="Meta information about the prompt" + ) enabled: bool = Field( default=True, description="Whether the component is enabled.", @@ -58,6 +60,10 @@ class FastMCPComponent(FastMCPBaseModel): """ return self._key or self.name + def get_meta(self) -> dict[str, Any]: + """Get the meta information about the component.""" + return {"tags": list(self.tags)} | self.meta + def with_key(self, key: str) -> Self: return self.model_copy(update={"_key": key}) diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index d97734a5a..eaf0211a6 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -268,6 +268,21 @@ class TestToolTags: result_2 = await client.call_tool("tool_2", {}) assert result_2.data == 2 + async def test_tags_in_meta(self): + mcp = FastMCP() + + @mcp.tool(tags={"tool-example", "test-tool-tag"}) + def sample_tool(x: int) -> int: + """A sample tool.""" + return x * 2 + + async with Client(mcp) as client: + tools = await client.list_tools() + assert len(tools) == 1 + tool = tools[0] + assert tool.meta is not None + assert set(tool.meta["tags"]) == {"tool-example", "test-tool-tag"} + class TestToolReturnTypes: async def test_string(self): @@ -1544,6 +1559,26 @@ class TestResourceTags: with pytest.raises(McpError, match="Unknown resource"): await client.read_resource(AnyUrl("resource://1")) + async def test_tags_in_meta(self): + mcp = FastMCP() + + @mcp.resource( + uri="test://resource", tags={"resource-example", "test-resource-tag"} + ) + def sample_resource() -> str: + """A sample resource.""" + return "resource content" + + async with Client(mcp) as client: + resources = await client.list_resources() + assert len(resources) == 1 + resource = resources[0] + assert resource.meta is not None + assert set(resource.meta["tags"]) == { + "resource-example", + "test-resource-tag", + } + class TestResourceContext: async def test_resource_with_context_annotation_gets_context(self): @@ -1972,6 +2007,26 @@ class TestResourceTemplatesTags: result = await client.read_resource("resource://2/x") assert result[0].text == "Template resource 2: x" # type: ignore[attr-defined] + async def test_tags_in_meta(self): + mcp = FastMCP() + + @mcp.resource( + "test://template/{id}", tags={"template-example", "test-template-tag"} + ) + def sample_template(id: str) -> str: + """A sample resource template.""" + return f"template content for {id}" + + async with Client(mcp) as client: + templates = await client.list_resource_templates() + assert len(templates) == 1 + template = templates[0] + assert template.meta is not None + assert set(template.meta["tags"]) == { + "template-example", + "test-template-tag", + } + class TestResourceTemplateContext: async def test_resource_template_context(self): @@ -2339,6 +2394,20 @@ class TestPrompts: prompt = prompts_dict["sample_prompt"] assert prompt.tags == {"example", "test-tag"} + async def test_tags_in_meta(self): + mcp = FastMCP() + + @mcp.prompt(tags={"example", "test-tag"}) + def sample_prompt() -> str: + return "Hello, world!" + + async with Client(mcp) as client: + prompts = await client.list_prompts() + assert len(prompts) == 1 + prompt = prompts[0] + assert prompt.meta is not None + assert set(prompt.meta["tags"]) == {"example", "test-tag"} + class TestPromptEnabled: async def test_toggle_enabled(self):