"""Tests for FastMCPProvider.""" import mcp.types as mt from fastmcp import FastMCP from fastmcp.client import Client from fastmcp.prompts.base import PromptResult from fastmcp.resources.base import ResourceResult from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext from fastmcp.server.providers import FastMCPProvider from fastmcp.tools.base import ToolResult class ToolTracingMiddleware(Middleware): """Middleware that traces tool calls.""" def __init__(self, name: str, calls: list[str]): super().__init__() self._name = name self._calls = calls async def on_call_tool( self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult], ) -> ToolResult: self._calls.append(f"{self._name}:before") result = await call_next(context) self._calls.append(f"{self._name}:after") return result class ResourceTracingMiddleware(Middleware): """Middleware that traces resource reads.""" def __init__(self, name: str, calls: list[str]): super().__init__() self._name = name self._calls = calls async def on_read_resource( self, context: MiddlewareContext[mt.ReadResourceRequestParams], call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult], ) -> ResourceResult: self._calls.append(f"{self._name}:before") result = await call_next(context) self._calls.append(f"{self._name}:after") return result class PromptTracingMiddleware(Middleware): """Middleware that traces prompt gets.""" def __init__(self, name: str, calls: list[str]): super().__init__() self._name = name self._calls = calls async def on_get_prompt( self, context: MiddlewareContext[mt.GetPromptRequestParams], call_next: CallNext[mt.GetPromptRequestParams, PromptResult], ) -> PromptResult: self._calls.append(f"{self._name}:before") result = await call_next(context) self._calls.append(f"{self._name}:after") return result class TestToolOperations: """Test tool operations through FastMCPProvider.""" async def test_list_tools(self): """Test listing tools from wrapped server.""" server = FastMCP("Test") @server.tool def tool_one() -> str: return "one" @server.tool def tool_two() -> str: return "two" provider = FastMCPProvider(server) tools = await provider.list_tools() assert len(tools) == 2 names = {t.name for t in tools} assert names == {"tool_one", "tool_two"} async def test_get_tool(self): """Test getting a specific tool by name.""" server = FastMCP("Test") @server.tool def my_tool() -> str: return "result" provider = FastMCPProvider(server) tool = await provider.get_tool("my_tool") assert tool is not None assert tool.name == "my_tool" async def test_get_nonexistent_tool_returns_none(self): """Test that getting a nonexistent tool returns None.""" server = FastMCP("Test") provider = FastMCPProvider(server) tool = await provider.get_tool("nonexistent") assert tool is None async def test_call_tool_via_client(self): """Test calling a tool through a server using the provider.""" sub = FastMCP("Sub") @sub.tool def greet(name: str) -> str: return f"Hello, {name}!" main = FastMCP("Main") main.add_provider(FastMCPProvider(sub)) async with Client(main) as client: result = await client.call_tool("greet", {"name": "World"}) assert result.data == "Hello, World!" class TestResourceOperations: """Test resource operations through FastMCPProvider.""" async def test_list_resources(self): """Test listing resources from wrapped server.""" server = FastMCP("Test") @server.resource("resource://one") def resource_one() -> str: return "one" @server.resource("resource://two") def resource_two() -> str: return "two" provider = FastMCPProvider(server) resources = await provider.list_resources() assert len(resources) == 2 uris = {str(r.uri) for r in resources} assert uris == {"resource://one", "resource://two"} async def test_get_resource(self): """Test getting a specific resource by URI.""" server = FastMCP("Test") @server.resource("resource://data") def my_resource() -> str: return "content" provider = FastMCPProvider(server) resource = await provider.get_resource("resource://data") assert resource is not None assert str(resource.uri) == "resource://data" async def test_read_resource_via_client(self): """Test reading a resource through a server using the provider.""" sub = FastMCP("Sub") @sub.resource("resource://data") def my_resource() -> str: return "content" main = FastMCP("Main") main.add_provider(FastMCPProvider(sub)) async with Client(main) as client: result = await client.read_resource("resource://data") assert isinstance(result[0], mt.TextResourceContents) assert result[0].text == "content" class TestResourceTemplateOperations: """Test resource template operations through FastMCPProvider.""" async def test_list_resource_templates(self): """Test listing resource templates from wrapped server.""" server = FastMCP("Test") @server.resource("resource://{id}/data") def my_template(id: str) -> str: return f"data for {id}" provider = FastMCPProvider(server) templates = await provider.list_resource_templates() assert len(templates) == 1 assert templates[0].uri_template == "resource://{id}/data" async def test_get_resource_template(self): """Test getting a template that matches a URI.""" server = FastMCP("Test") @server.resource("resource://{id}/data") def my_template(id: str) -> str: return f"data for {id}" provider = FastMCPProvider(server) template = await provider.get_resource_template("resource://123/data") assert template is not None async def test_read_resource_template_via_client(self): """Test reading a resource via template through a server using the provider.""" sub = FastMCP("Sub") @sub.resource("resource://{id}/data") def my_template(id: str) -> str: return f"data for {id}" main = FastMCP("Main") main.add_provider(FastMCPProvider(sub)) async with Client(main) as client: result = await client.read_resource("resource://123/data") assert isinstance(result[0], mt.TextResourceContents) assert result[0].text == "data for 123" class TestPromptOperations: """Test prompt operations through FastMCPProvider.""" async def test_list_prompts(self): """Test listing prompts from wrapped server.""" server = FastMCP("Test") @server.prompt def prompt_one() -> str: return "one" @server.prompt def prompt_two() -> str: return "two" provider = FastMCPProvider(server) prompts = await provider.list_prompts() assert len(prompts) == 2 names = {p.name for p in prompts} assert names == {"prompt_one", "prompt_two"} async def test_get_prompt(self): """Test getting a specific prompt by name.""" server = FastMCP("Test") @server.prompt def my_prompt() -> str: return "content" provider = FastMCPProvider(server) prompt = await provider.get_prompt("my_prompt") assert prompt is not None assert prompt.name == "my_prompt" async def test_render_prompt_via_client(self): """Test rendering a prompt through a server using the provider.""" sub = FastMCP("Sub") @sub.prompt def greet(name: str) -> str: return f"Hello, {name}!" main = FastMCP("Main") main.add_provider(FastMCPProvider(sub)) async with Client(main) as client: result = await client.get_prompt("greet", {"name": "World"}) assert isinstance(result.messages[0].content, mt.TextContent) assert result.messages[0].content.text == "Hello, World!" class TestServerReference: """Test that provider maintains reference to wrapped server.""" def test_server_attribute(self): """Test that provider exposes the wrapped server.""" server = FastMCP("Test") provider = FastMCPProvider(server) assert provider.server is server def test_server_name_accessible(self): """Test that server name is accessible through provider.""" server = FastMCP("MyServer") provider = FastMCPProvider(server) assert provider.server.name == "MyServer" class TestMiddlewareChain: """Test that middleware runs at each level of mounted servers.""" async def test_tool_middleware_three_levels(self): """Middleware runs at parent, child, and grandchild levels for tools.""" calls: list[str] = [] grandchild = FastMCP("Grandchild") @grandchild.tool async def compute(x: int) -> int: calls.append("grandchild:tool") return x * 2 grandchild.add_middleware(ToolTracingMiddleware("grandchild", calls)) child = FastMCP("Child") child.mount(grandchild, namespace="gc") child.add_middleware(ToolTracingMiddleware("child", calls)) parent = FastMCP("Parent") parent.mount(child, namespace="c") parent.add_middleware(ToolTracingMiddleware("parent", calls)) async with Client(parent) as client: result = await client.call_tool("c_gc_compute", {"x": 5}) assert result.data == 10 assert calls == [ "parent:before", "child:before", "grandchild:before", "grandchild:tool", "grandchild:after", "child:after", "parent:after", ] async def test_resource_middleware_three_levels(self): """Middleware runs at parent, child, and grandchild levels for resources.""" calls: list[str] = [] grandchild = FastMCP("Grandchild") @grandchild.resource("data://value") async def get_data() -> str: calls.append("grandchild:resource") return "result" grandchild.add_middleware(ResourceTracingMiddleware("grandchild", calls)) child = FastMCP("Child") child.mount(grandchild, namespace="gc") child.add_middleware(ResourceTracingMiddleware("child", calls)) parent = FastMCP("Parent") parent.mount(child, namespace="c") parent.add_middleware(ResourceTracingMiddleware("parent", calls)) async with Client(parent) as client: result = await client.read_resource("data://c/gc/value") assert isinstance(result[0], mt.TextResourceContents) assert result[0].text == "result" assert calls == [ "parent:before", "child:before", "grandchild:before", "grandchild:resource", "grandchild:after", "child:after", "parent:after", ] async def test_prompt_middleware_three_levels(self): """Middleware runs at parent, child, and grandchild levels for prompts.""" calls: list[str] = [] grandchild = FastMCP("Grandchild") @grandchild.prompt async def greet(name: str) -> str: calls.append("grandchild:prompt") return f"Hello, {name}!" grandchild.add_middleware(PromptTracingMiddleware("grandchild", calls)) child = FastMCP("Child") child.mount(grandchild, namespace="gc") child.add_middleware(PromptTracingMiddleware("child", calls)) parent = FastMCP("Parent") parent.mount(child, namespace="c") parent.add_middleware(PromptTracingMiddleware("parent", calls)) async with Client(parent) as client: result = await client.get_prompt("c_gc_greet", {"name": "World"}) assert isinstance(result.messages[0].content, mt.TextContent) assert result.messages[0].content.text == "Hello, World!" assert calls == [ "parent:before", "child:before", "grandchild:before", "grandchild:prompt", "grandchild:after", "child:after", "parent:after", ] async def test_resource_template_middleware_three_levels(self): """Middleware runs at all levels for resource templates.""" calls: list[str] = [] grandchild = FastMCP("Grandchild") @grandchild.resource("item://{id}") async def get_item(id: str) -> str: calls.append("grandchild:template") return f"item-{id}" grandchild.add_middleware(ResourceTracingMiddleware("grandchild", calls)) child = FastMCP("Child") child.mount(grandchild, namespace="gc") child.add_middleware(ResourceTracingMiddleware("child", calls)) parent = FastMCP("Parent") parent.mount(child, namespace="c") parent.add_middleware(ResourceTracingMiddleware("parent", calls)) async with Client(parent) as client: result = await client.read_resource("item://c/gc/42") assert isinstance(result[0], mt.TextResourceContents) assert result[0].text == "item-42" assert calls == [ "parent:before", "child:before", "grandchild:before", "grandchild:template", "grandchild:after", "child:after", "parent:after", ]