diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py index e77964cc0..8cb0b3f4e 100644 --- a/tests/server/middleware/test_caching.py +++ b/tests/server/middleware/test_caching.py @@ -521,3 +521,96 @@ class TestCachableToolResult: assert cached_tool_result.content == tool_result.content assert cached_tool_result.structured_content == tool_result.structured_content assert cached_tool_result.meta == tool_result.meta + + +class TestCachingWithImportedServerPrefixes: + """Test that caching preserves prefixes from imported servers. + + Regression tests for issue #2300: ResponseCachingMiddleware was losing + prefix information when caching components from imported servers. + """ + + @pytest.fixture + async def parent_with_imported_child(self, tracking_calculator: TrackingCalculator): + """Create a parent server with an imported child server (prefixed).""" + child = FastMCP("child") + tracking_calculator.add_tools(fastmcp=child) + tracking_calculator.add_resources(fastmcp=child) + tracking_calculator.add_prompts(fastmcp=child) + + parent = FastMCP("parent") + parent.add_middleware(ResponseCachingMiddleware()) + parent.mount(child, namespace="child") + + return parent + + async def test_tool_prefixes_preserved_after_cache_hit( + self, parent_with_imported_child: FastMCP + ): + """Tool names should retain prefix after being served from cache.""" + async with Client(parent_with_imported_child) as client: + # First call populates cache + tools_first = await client.list_tools() + tool_names_first = [t.name for t in tools_first] + + # Second call should come from cache + tools_cached = await client.list_tools() + tool_names_cached = [t.name for t in tools_cached] + + # All tools should have prefix in both calls + assert all(name.startswith("child_") for name in tool_names_first) + assert all(name.startswith("child_") for name in tool_names_cached) + assert tool_names_first == tool_names_cached + + async def test_resource_prefixes_preserved_after_cache_hit( + self, parent_with_imported_child: FastMCP + ): + """Resource URIs should retain prefix after being served from cache.""" + async with Client(parent_with_imported_child) as client: + # First call populates cache + resources_first = await client.list_resources() + resource_uris_first = [str(r.uri) for r in resources_first] + + # Second call should come from cache + resources_cached = await client.list_resources() + resource_uris_cached = [str(r.uri) for r in resources_cached] + + # All resources should have prefix in URI path in both calls + # Resources get path-style prefix: resource://child/path + assert all("://child/" in uri for uri in resource_uris_first) + assert all("://child/" in uri for uri in resource_uris_cached) + assert resource_uris_first == resource_uris_cached + + async def test_prompt_prefixes_preserved_after_cache_hit( + self, parent_with_imported_child: FastMCP + ): + """Prompt names should retain prefix after being served from cache.""" + async with Client(parent_with_imported_child) as client: + # First call populates cache + prompts_first = await client.list_prompts() + prompt_names_first = [p.name for p in prompts_first] + + # Second call should come from cache + prompts_cached = await client.list_prompts() + prompt_names_cached = [p.name for p in prompts_cached] + + # All prompts should have prefix in both calls + assert all(name.startswith("child_") for name in prompt_names_first) + assert all(name.startswith("child_") for name in prompt_names_cached) + assert prompt_names_first == prompt_names_cached + + async def test_prefixed_tool_callable_after_cache_hit( + self, + parent_with_imported_child: FastMCP, + tracking_calculator: TrackingCalculator, + ): + """Prefixed tools should be callable after cache populates.""" + async with Client(parent_with_imported_child) as client: + # Trigger cache population + await client.list_tools() + await client.list_tools() # From cache + + # Tool should be callable with prefixed name + result = await client.call_tool("child_add", {"a": 5, "b": 3}) + assert not result.is_error + assert tracking_calculator.add_calls == 1