diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py index 24fb6c680..15e7d03a7 100644 --- a/src/fastmcp/server/proxy.py +++ b/src/fastmcp/server/proxy.py @@ -255,6 +255,8 @@ class ProxyTool(Tool, MirroredComponent): parameters=mcp_tool.inputSchema, annotations=mcp_tool.annotations, output_schema=mcp_tool.outputSchema, + meta=mcp_tool.meta, + tags=(mcp_tool.meta or {}).get("tags", []), _mirrored=True, ) @@ -309,6 +311,8 @@ class ProxyResource(Resource, MirroredComponent): name=mcp_resource.name, description=mcp_resource.description, mime_type=mcp_resource.mimeType or "text/plain", + meta=mcp_resource.meta, + tags=(mcp_resource.meta or {}).get("tags", []), _mirrored=True, ) @@ -348,6 +352,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent): description=mcp_template.description, mime_type=mcp_template.mimeType or "text/plain", parameters={}, # Remote templates don't have local parameters + meta=mcp_template.meta, + tags=(mcp_template.meta or {}).get("tags", []), _mirrored=True, ) @@ -380,6 +386,8 @@ class ProxyTemplate(ResourceTemplate, MirroredComponent): name=self.name, description=self.description, mime_type=result[0].mimeType, + meta=self.meta, + tags=(self.meta or {}).get("tags", []), _value=value, ) @@ -413,6 +421,8 @@ class ProxyPrompt(Prompt, MirroredComponent): name=mcp_prompt.name, description=mcp_prompt.description, arguments=arguments, + meta=mcp_prompt.meta, + tags=(mcp_prompt.meta or {}).get("tags", []), _mirrored=True, ) diff --git a/tests/server/proxy/test_proxy_client.py b/tests/server/proxy/test_proxy_client.py index 6239a9e85..331252694 100644 --- a/tests/server/proxy/test_proxy_client.py +++ b/tests/server/proxy/test_proxy_client.py @@ -18,6 +18,10 @@ from fastmcp.server.proxy import ProxyClient def fastmcp_server(): mcp = FastMCP("TestServer") + @mcp.tool(tags={"echo"}) + def echo(message: str) -> str: + return f"echo: {message}" + @mcp.tool async def list_roots(context: Context) -> list[str]: roots = await context.list_roots() @@ -80,6 +84,15 @@ async def proxy_server(fastmcp_server: FastMCP): class TestProxyClient: + async def test_forward_tool_meta(self, proxy_server: FastMCP): + """ + Test that the proxy client correctly forwards the `echo` tool meta. + """ + async with Client(proxy_server) as client: + tools = await client.list_tools() + echo_tool = next(t for t in tools if t.name == "echo") + assert echo_tool.meta == {"tags": ["echo"]} + async def test_forward_error_response(self, proxy_server: FastMCP): """ Test that the proxy client correctly forwards an error response. diff --git a/tests/server/proxy/test_proxy_server.py b/tests/server/proxy/test_proxy_server.py index dea8d3259..42e0cb51b 100644 --- a/tests/server/proxy/test_proxy_server.py +++ b/tests/server/proxy/test_proxy_server.py @@ -29,7 +29,7 @@ def fastmcp_server(): # --- Tools --- - @server.tool + @server.tool(tags={"greet"}) def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" @@ -50,7 +50,7 @@ def fastmcp_server(): # --- Resources --- - @server.resource(uri="resource://wave") + @server.resource(uri="resource://wave", tags={"wave"}) def wave() -> str: return "👋" @@ -58,13 +58,13 @@ def fastmcp_server(): async def get_users() -> list[dict[str, Any]]: return USERS - @server.resource(uri="data://user/{user_id}") + @server.resource(uri="data://user/{user_id}", tags={"users"}) async def get_user(user_id: str) -> dict[str, Any] | None: return next((user for user in USERS if user["id"] == user_id), None) # --- Prompts --- - @server.prompt + @server.prompt(tags={"welcome"}) def welcome(name: str) -> str: return f"Welcome to FastMCP, {name}!" @@ -121,6 +121,11 @@ class TestTools: assert "error_tool" in tools assert "tool_without_description" in tools + async def test_get_tools_meta(self, proxy_server): + tools = await proxy_server.get_tools() + greet_tool = tools["greet"] + assert greet_tool.meta == {"tags": ["greet"]} + async def test_get_transformed_tools( self, fastmcp_server: FastMCP, proxy_server: FastMCPProxy ): @@ -238,6 +243,11 @@ class TestResources: ) assert [r.name for r in resources.values()] == Contains("get_users", "wave") + async def test_get_resources_meta(self, proxy_server): + resources = await proxy_server.get_resources() + wave_resource = resources["resource://wave"] + assert wave_resource.meta == {"tags": ["wave"]} + async def test_list_resources_same_as_original(self, fastmcp_server, proxy_server): assert ( await proxy_server._mcp_list_resources() @@ -332,6 +342,11 @@ class TestResourceTemplates: templates = await proxy_server.get_resource_templates() assert [t.name for t in templates.values()] == Contains("get_user") + async def test_get_resource_templates_meta(self, proxy_server): + templates = await proxy_server.get_resource_templates() + get_user_template = templates["data://user/{user_id}"] + assert get_user_template.meta == {"tags": ["users"]} + async def test_list_resource_templates_same_as_original( self, fastmcp_server, proxy_server ): @@ -431,6 +446,11 @@ class TestPrompts: prompts = await proxy_server.get_prompts() assert [p.name for p in prompts.values()] == Contains("welcome") + async def test_get_prompts_meta(self, proxy_server): + prompts = await proxy_server.get_prompts() + welcome_prompt = prompts["welcome"] + assert welcome_prompt.meta == {"tags": ["welcome"]} + async def test_list_prompts_same_as_original(self, fastmcp_server, proxy_server): async with Client(fastmcp_server) as client: result = await client.list_prompts()