Skip to content

Commit cda1cf4

Browse files
committed
test: add comprehensive unit tests for tool metadata feature
Add tests covering: - Adding metadata via ToolManager.add_tool() - Adding metadata via FastMCP.tool() decorator - Metadata preservation in Tool objects - Metadata inclusion in MCPTool when listing tools - Edge cases (None, empty dict, complex nested structures) - Integration with tool annotations All tests verify that the meta parameter correctly flows through the tool registration and listing pipeline.
1 parent 2c2cd0e commit cda1cf4

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed

tests/server/fastmcp/test_tool_manager.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,178 @@ def get_scores() -> dict[str, int]:
635635
assert result == expected
636636

637637

638+
class TestToolMetadata:
639+
"""Test tool metadata functionality."""
640+
641+
def test_add_tool_with_metadata(self):
642+
"""Test adding a tool with metadata via ToolManager."""
643+
644+
def process_data(input_data: str) -> str:
645+
"""Process some data."""
646+
return f"Processed: {input_data}"
647+
648+
metadata = {"ui": {"type": "form", "fields": ["input"]}, "version": "1.0"}
649+
650+
manager = ToolManager()
651+
tool = manager.add_tool(process_data, meta=metadata)
652+
653+
assert tool.meta is not None
654+
assert tool.meta == metadata
655+
assert tool.meta["ui"]["type"] == "form"
656+
assert tool.meta["version"] == "1.0"
657+
658+
def test_add_tool_without_metadata(self):
659+
"""Test that tools without metadata have None as meta value."""
660+
661+
def simple_tool(x: int) -> int:
662+
"""Simple tool."""
663+
return x * 2
664+
665+
manager = ToolManager()
666+
tool = manager.add_tool(simple_tool)
667+
668+
assert tool.meta is None
669+
670+
@pytest.mark.anyio
671+
async def test_metadata_in_fastmcp_decorator(self):
672+
"""Test that metadata is correctly added via FastMCP.tool decorator."""
673+
674+
app = FastMCP()
675+
676+
metadata = {"client": {"ui_component": "file_picker"}, "priority": "high"}
677+
678+
@app.tool(meta=metadata)
679+
def upload_file(filename: str) -> str:
680+
"""Upload a file."""
681+
return f"Uploaded: {filename}"
682+
683+
# Get the tool from the tool manager
684+
tool = app._tool_manager.get_tool("upload_file")
685+
assert tool is not None
686+
assert tool.meta is not None
687+
assert tool.meta == metadata
688+
assert tool.meta["client"]["ui_component"] == "file_picker"
689+
assert tool.meta["priority"] == "high"
690+
691+
@pytest.mark.anyio
692+
async def test_metadata_in_list_tools(self):
693+
"""Test that metadata is included in MCPTool when listing tools."""
694+
695+
app = FastMCP()
696+
697+
metadata = {
698+
"ui": {"input_type": "textarea", "rows": 5},
699+
"tags": ["text", "processing"],
700+
}
701+
702+
@app.tool(meta=metadata)
703+
def analyze_text(text: str) -> dict[str, Any]:
704+
"""Analyze text content."""
705+
return {"length": len(text), "words": len(text.split())}
706+
707+
tools = await app.list_tools()
708+
assert len(tools) == 1
709+
assert tools[0].meta is not None
710+
assert tools[0].meta == metadata
711+
712+
@pytest.mark.anyio
713+
async def test_multiple_tools_with_different_metadata(self):
714+
"""Test multiple tools with different metadata values."""
715+
716+
app = FastMCP()
717+
718+
metadata1 = {"ui": "form", "version": 1}
719+
metadata2 = {"ui": "picker", "experimental": True}
720+
721+
@app.tool(meta=metadata1)
722+
def tool1(x: int) -> int:
723+
"""First tool."""
724+
return x
725+
726+
@app.tool(meta=metadata2)
727+
def tool2(y: str) -> str:
728+
"""Second tool."""
729+
return y
730+
731+
@app.tool()
732+
def tool3(z: bool) -> bool:
733+
"""Third tool without metadata."""
734+
return z
735+
736+
tools = await app.list_tools()
737+
assert len(tools) == 3
738+
739+
# Find tools by name and check metadata
740+
tools_by_name = {t.name: t for t in tools}
741+
742+
assert tools_by_name["tool1"].meta == metadata1
743+
assert tools_by_name["tool2"].meta == metadata2
744+
assert tools_by_name["tool3"].meta is None
745+
746+
def test_metadata_with_complex_structure(self):
747+
"""Test metadata with complex nested structures."""
748+
749+
def complex_tool(data: str) -> str:
750+
"""Tool with complex metadata."""
751+
return data
752+
753+
metadata = {
754+
"ui": {
755+
"components": [
756+
{"type": "input", "name": "field1", "validation": {"required": True, "minLength": 5}},
757+
{"type": "select", "name": "field2", "options": ["a", "b", "c"]},
758+
],
759+
"layout": {"columns": 2, "responsive": True},
760+
},
761+
"permissions": ["read", "write"],
762+
"tags": ["data-processing", "user-input"],
763+
"version": 2,
764+
}
765+
766+
manager = ToolManager()
767+
tool = manager.add_tool(complex_tool, meta=metadata)
768+
769+
assert tool.meta is not None
770+
assert tool.meta["ui"]["components"][0]["validation"]["minLength"] == 5
771+
assert tool.meta["ui"]["layout"]["columns"] == 2
772+
assert "read" in tool.meta["permissions"]
773+
assert "data-processing" in tool.meta["tags"]
774+
775+
def test_metadata_empty_dict(self):
776+
"""Test that empty dict metadata is preserved."""
777+
778+
def tool_with_empty_meta(x: int) -> int:
779+
"""Tool with empty metadata."""
780+
return x
781+
782+
manager = ToolManager()
783+
tool = manager.add_tool(tool_with_empty_meta, meta={})
784+
785+
assert tool.meta is not None
786+
assert tool.meta == {}
787+
788+
@pytest.mark.anyio
789+
async def test_metadata_with_annotations(self):
790+
"""Test that metadata and annotations can coexist."""
791+
792+
app = FastMCP()
793+
794+
metadata = {"custom": "value"}
795+
annotations = ToolAnnotations(title="Combined Tool", readOnlyHint=True)
796+
797+
@app.tool(meta=metadata, annotations=annotations)
798+
def combined_tool(data: str) -> str:
799+
"""Tool with both metadata and annotations."""
800+
return data
801+
802+
tools = await app.list_tools()
803+
assert len(tools) == 1
804+
assert tools[0].meta == metadata
805+
assert tools[0].annotations is not None
806+
assert tools[0].annotations.title == "Combined Tool"
807+
assert tools[0].annotations.readOnlyHint is True
808+
809+
638810
class TestRemoveTools:
639811
"""Test tool removal functionality in the tool manager."""
640812

0 commit comments

Comments
 (0)