fastmcp/tests/utilities/test_inspect.py
2025-08-06 11:23:34 -04:00

392 lines
13 KiB
Python

"""Tests for the inspect.py module."""
import importlib.metadata
from mcp.server.fastmcp import FastMCP as FastMCP1x
import fastmcp
from fastmcp import Client, FastMCP
from fastmcp.utilities.inspect import (
FastMCPInfo,
ToolInfo,
inspect_fastmcp,
inspect_fastmcp_v1,
)
class TestFastMCPInfo:
"""Tests for the FastMCPInfo dataclass."""
def test_fastmcp_info_creation(self):
"""Test that FastMCPInfo can be created with all required fields."""
tool = ToolInfo(
key="tool1", name="tool1", description="Test tool", input_schema={}
)
info = FastMCPInfo(
name="TestServer",
instructions="Test instructions",
fastmcp_version="1.0.0",
mcp_version="1.0.0",
server_version="1.0.0",
tools=[tool],
prompts=[],
resources=[],
templates=[],
capabilities={"tools": {"listChanged": True}},
)
assert info.name == "TestServer"
assert info.instructions == "Test instructions"
assert info.fastmcp_version == "1.0.0"
assert info.mcp_version == "1.0.0"
assert info.server_version == "1.0.0"
assert len(info.tools) == 1
assert info.tools[0].name == "tool1"
assert info.capabilities == {"tools": {"listChanged": True}}
def test_fastmcp_info_with_none_instructions(self):
"""Test that FastMCPInfo works with None instructions."""
info = FastMCPInfo(
name="TestServer",
instructions=None,
fastmcp_version="1.0.0",
mcp_version="1.0.0",
server_version="1.0.0",
tools=[],
prompts=[],
resources=[],
templates=[],
capabilities={},
)
assert info.instructions is None
class TestGetFastMCPInfo:
"""Tests for the get_fastmcp_info function."""
async def test_empty_server(self):
"""Test get_fastmcp_info with an empty server."""
mcp = FastMCP("EmptyServer")
info = await inspect_fastmcp(mcp)
assert info.name == "EmptyServer"
assert info.instructions is None
assert info.fastmcp_version == fastmcp.__version__
assert info.mcp_version == importlib.metadata.version("mcp")
assert info.server_version is None
assert info.tools == []
assert info.prompts == []
assert info.resources == []
assert info.templates == []
assert "tools" in info.capabilities
assert "resources" in info.capabilities
assert "prompts" in info.capabilities
assert "logging" in info.capabilities
async def test_server_with_instructions(self):
"""Test get_fastmcp_info with a server that has instructions."""
mcp = FastMCP("InstructionsServer", instructions="Test instructions")
info = await inspect_fastmcp(mcp)
assert info.instructions == "Test instructions"
async def test_server_with_version(self):
"""Test get_fastmcp_info with a server that has a version."""
mcp = FastMCP("VersionServer", version="1.2.3")
info = await inspect_fastmcp(mcp)
assert info.server_version == "1.2.3"
async def test_server_with_tools(self):
"""Test get_fastmcp_info with a server that has tools."""
mcp = FastMCP("ToolServer")
@mcp.tool
def add_numbers(a: int, b: int) -> int:
return a + b
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
info = await inspect_fastmcp(mcp)
assert info.name == "ToolServer"
assert len(info.tools) == 2
tool_names = [tool.name for tool in info.tools]
assert "add_numbers" in tool_names
assert "greet" in tool_names
async def test_server_with_resources(self):
"""Test get_fastmcp_info with a server that has resources."""
mcp = FastMCP("ResourceServer")
@mcp.resource("resource://static")
def get_static_data() -> str:
return "Static data"
@mcp.resource("resource://dynamic/{param}")
def get_dynamic_data(param: str) -> str:
return f"Dynamic data: {param}"
info = await inspect_fastmcp(mcp)
assert info.name == "ResourceServer"
assert len(info.resources) == 1 # Static resource
assert len(info.templates) == 1 # Dynamic resource becomes template
resource_uris = [res.uri for res in info.resources]
template_uris = [tmpl.uri_template for tmpl in info.templates]
assert "resource://static" in resource_uris
assert "resource://dynamic/{param}" in template_uris
async def test_server_with_prompts(self):
"""Test get_fastmcp_info with a server that has prompts."""
mcp = FastMCP("PromptServer")
@mcp.prompt
def analyze_data(data: str) -> list:
return [{"role": "user", "content": f"Analyze: {data}"}]
@mcp.prompt("custom_prompt")
def custom_analysis(text: str) -> list:
return [{"role": "user", "content": f"Custom: {text}"}]
info = await inspect_fastmcp(mcp)
assert info.name == "PromptServer"
assert len(info.prompts) == 2
prompt_names = [prompt.name for prompt in info.prompts]
assert "analyze_data" in prompt_names
assert "custom_prompt" in prompt_names
async def test_comprehensive_server(self):
"""Test get_fastmcp_info with a server that has all component types."""
mcp = FastMCP("ComprehensiveServer", instructions="A server with everything")
# Add a tool
@mcp.tool
def calculate(x: int, y: int) -> int:
return x * y
# Add a resource
@mcp.resource("resource://data")
def get_data() -> str:
return "Some data"
# Add a template
@mcp.resource("resource://item/{id}")
def get_item(id: str) -> str:
return f"Item {id}"
# Add a prompt
@mcp.prompt
def analyze(content: str) -> list:
return [{"role": "user", "content": content}]
info = await inspect_fastmcp(mcp)
assert info.name == "ComprehensiveServer"
assert info.instructions == "A server with everything"
assert info.fastmcp_version == fastmcp.__version__
# Check all components are present
assert len(info.tools) == 1
tool_names = [tool.name for tool in info.tools]
assert "calculate" in tool_names
assert len(info.resources) == 1
resource_uris = [res.uri for res in info.resources]
assert "resource://data" in resource_uris
assert len(info.templates) == 1
template_uris = [tmpl.uri_template for tmpl in info.templates]
assert "resource://item/{id}" in template_uris
assert len(info.prompts) == 1
prompt_names = [prompt.name for prompt in info.prompts]
assert "analyze" in prompt_names
# Check capabilities
assert "tools" in info.capabilities
assert "resources" in info.capabilities
assert "prompts" in info.capabilities
assert "logging" in info.capabilities
async def test_server_no_instructions(self):
"""Test get_fastmcp_info with a server that has no instructions."""
mcp = FastMCP("NoInstructionsServer")
info = await inspect_fastmcp(mcp)
assert info.name == "NoInstructionsServer"
assert info.instructions is None
async def test_server_with_client_integration(self):
"""Test that the extracted info matches what a client would see."""
mcp = FastMCP("IntegrationServer")
@mcp.tool
def test_tool() -> str:
return "test"
@mcp.resource("resource://test")
def test_resource() -> str:
return "test resource"
@mcp.prompt
def test_prompt() -> list:
return [{"role": "user", "content": "test"}]
# Get info using our function
info = await inspect_fastmcp(mcp)
# Verify using client
async with Client(mcp) as client:
tools = await client.list_tools()
resources = await client.list_resources()
prompts = await client.list_prompts()
assert len(info.tools) == len(tools)
assert len(info.resources) == len(resources)
assert len(info.prompts) == len(prompts)
assert info.tools[0].name == tools[0].name
assert info.resources[0].uri == str(resources[0].uri)
assert info.prompts[0].name == prompts[0].name
class TestFastMCP1xCompatibility:
"""Tests for FastMCP 1.x compatibility."""
async def test_fastmcp1x_empty_server(self):
"""Test get_fastmcp_info_v1 with an empty FastMCP1x server."""
mcp = FastMCP1x("Test1x")
info = await inspect_fastmcp_v1(mcp)
assert info.name == "Test1x"
assert info.instructions is None
assert info.fastmcp_version == importlib.metadata.version("mcp")
assert info.mcp_version == importlib.metadata.version("mcp")
assert info.server_version is None
assert info.tools == []
assert info.prompts == []
assert info.resources == []
assert info.templates == [] # No templates added in this test
assert "tools" in info.capabilities
async def test_fastmcp1x_with_tools(self):
"""Test get_fastmcp_info_v1 with a FastMCP1x server that has tools."""
mcp = FastMCP1x("Test1x")
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
return a + b
@mcp.tool()
def greet(name: str) -> str:
return f"Hello, {name}!"
info = await inspect_fastmcp_v1(mcp)
assert info.name == "Test1x"
assert len(info.tools) == 2
tool_names = [tool.name for tool in info.tools]
assert "add_numbers" in tool_names
assert "greet" in tool_names
async def test_fastmcp1x_with_resources(self):
"""Test get_fastmcp_info_v1 with a FastMCP1x server that has resources."""
mcp = FastMCP1x("Test1x")
@mcp.resource("resource://data")
def get_data() -> str:
return "Some data"
info = await inspect_fastmcp_v1(mcp)
assert info.name == "Test1x"
assert len(info.resources) == 1
resource_uris = [res.uri for res in info.resources]
assert "resource://data" in resource_uris
assert len(info.templates) == 0 # No templates added in this test
async def test_fastmcp1x_with_prompts(self):
"""Test get_fastmcp_info_v1 with a FastMCP1x server that has prompts."""
mcp = FastMCP1x("Test1x")
@mcp.prompt("analyze")
def analyze_data(data: str) -> list:
return [{"role": "user", "content": f"Analyze: {data}"}]
info = await inspect_fastmcp_v1(mcp)
assert info.name == "Test1x"
assert len(info.prompts) == 1
prompt_names = [prompt.name for prompt in info.prompts]
assert "analyze" in prompt_names
async def test_dispatcher_with_fastmcp1x(self):
"""Test that the main get_fastmcp_info function correctly dispatches to v1."""
mcp = FastMCP1x("Test1x")
@mcp.tool()
def test_tool() -> str:
return "test"
info = await inspect_fastmcp(mcp)
assert info.name == "Test1x"
assert len(info.tools) == 1
tool_names = [tool.name for tool in info.tools]
assert "test_tool" in tool_names
assert len(info.templates) == 0 # No templates added in this test
async def test_dispatcher_with_fastmcp2x(self):
"""Test that the main get_fastmcp_info function correctly dispatches to v2."""
mcp = FastMCP("Test2x")
@mcp.tool
def test_tool() -> str:
return "test"
info = await inspect_fastmcp(mcp)
assert info.name == "Test2x"
assert len(info.tools) == 1
tool_names = [tool.name for tool in info.tools]
assert "test_tool" in tool_names
async def test_fastmcp1x_vs_fastmcp2x_comparison(self):
"""Test that both versions can be inspected and compared."""
mcp1x = FastMCP1x("Test1x")
mcp2x = FastMCP("Test2x")
@mcp1x.tool()
def tool1x() -> str:
return "1x"
@mcp2x.tool
def tool2x() -> str:
return "2x"
info1x = await inspect_fastmcp(mcp1x)
info2x = await inspect_fastmcp(mcp2x)
assert info1x.name == "Test1x"
assert info2x.name == "Test2x"
assert len(info1x.tools) == 1
assert len(info2x.tools) == 1
tool1x_names = [tool.name for tool in info1x.tools]
tool2x_names = [tool.name for tool in info2x.tools]
assert "tool1x" in tool1x_names
assert "tool2x" in tool2x_names
# Check server versions
assert info1x.server_version is None
assert info2x.server_version is None
# No templates added in these tests
assert len(info1x.templates) == 0
assert len(info2x.templates) == 0