Skip to main content
Glama
test_tool_annotations.py5.93 kB
""" Tests for tool annotations. This module verifies that MCP tools have correct annotations for read-only, destructive, idempotent, and open-world operations. """ import pytest from mcp.server.fastmcp import FastMCP from mcp_outline.features import register_all @pytest.fixture def fresh_mcp_server(): """Create a fresh MCP server instance for testing.""" return FastMCP("Test Server") @pytest.mark.anyio async def test_read_only_tools_have_correct_annotations(fresh_mcp_server): """Test that read-only tools have readOnlyHint=True.""" register_all(fresh_mcp_server) tools = await fresh_mcp_server.list_tools() # Define expected read-only tools read_only_tools = [ "search_documents", "read_document", "list_document_comments", "export_collection", "get_comment", "list_collections", "get_collection_structure", "export_document", "get_document_backlinks", "list_archived_documents", "list_trash", "export_all_collections", ] for tool_name in read_only_tools: tool = next((t for t in tools if t.name == tool_name), None) assert tool is not None, f"Tool {tool_name} not found" assert tool.annotations is not None, ( f"Tool {tool_name} has no annotations" ) assert tool.annotations.readOnlyHint is True, ( f"Tool {tool_name} should have readOnlyHint=True" ) @pytest.mark.anyio async def test_destructive_tools_have_correct_annotations(fresh_mcp_server): """Test that destructive tools have destructiveHint=True.""" register_all(fresh_mcp_server) tools = await fresh_mcp_server.list_tools() # Define expected destructive tools destructive_tools = [ "delete_document", "delete_collection", "update_document", "move_document", "batch_delete_documents", "update_collection", "archive_document", "batch_archive_documents", "batch_move_documents", "batch_update_documents", "batch_create_documents", ] for tool_name in destructive_tools: tool = next((t for t in tools if t.name == tool_name), None) # Skip if tool not found (may be conditionally registered) if tool is None: continue assert tool.annotations is not None, ( f"Tool {tool_name} has no annotations" ) assert tool.annotations.destructiveHint is True, ( f"Tool {tool_name} should have destructiveHint=True" ) @pytest.mark.anyio async def test_non_destructive_write_tools(fresh_mcp_server): """Test non-destructive write tools have correct annotations.""" register_all(fresh_mcp_server) tools = await fresh_mcp_server.list_tools() # Define non-destructive write tools non_destructive_write_tools = [ "create_document", "add_comment", "create_collection", "unarchive_document", "restore_document", ] for tool_name in non_destructive_write_tools: tool = next((t for t in tools if t.name == tool_name), None) # Skip if tool not found (may be conditionally registered) if tool is None: continue assert tool.annotations is not None, ( f"Tool {tool_name} has no annotations" ) assert tool.annotations.readOnlyHint is False, ( f"Tool {tool_name} should have readOnlyHint=False" ) assert tool.annotations.destructiveHint is False, ( f"Tool {tool_name} should have destructiveHint=False" ) @pytest.mark.anyio async def test_idempotent_tools(fresh_mcp_server): """Test that idempotent tools have idempotentHint=True.""" register_all(fresh_mcp_server) tools = await fresh_mcp_server.list_tools() # Define expected idempotent tools idempotent_tools = [ "delete_document", "archive_document", "search_documents", "batch_delete_documents", "read_document", "list_collections", "get_collection_structure", "export_document", "list_document_comments", "get_comment", "get_document_backlinks", "unarchive_document", "restore_document", "list_archived_documents", "list_trash", "export_collection", "export_all_collections", "delete_collection", "batch_archive_documents", "batch_move_documents", "batch_update_documents", "batch_create_documents", ] for tool_name in idempotent_tools: tool = next((t for t in tools if t.name == tool_name), None) # Skip if tool not found (may be conditionally registered) if tool is None: continue assert tool.annotations is not None, ( f"Tool {tool_name} has no annotations" ) assert tool.annotations.idempotentHint is True, ( f"Tool {tool_name} should have idempotentHint=True" ) @pytest.mark.anyio async def test_ai_tools_have_open_world_hint(fresh_mcp_server): """Test that AI tools have openWorldHint=True.""" register_all(fresh_mcp_server) tools = await fresh_mcp_server.list_tools() # Only AI tools should have openWorldHint (can discover new insights) # Regular search works with known entities, so no openWorldHint ai_tools = ["ask_ai_about_documents"] for tool_name in ai_tools: tool = next((t for t in tools if t.name == tool_name), None) # Skip if tool not found (may be conditionally registered) if tool is None: continue assert tool.annotations is not None, ( f"Tool {tool_name} has no annotations" ) assert tool.annotations.openWorldHint is True, ( f"Tool {tool_name} should have openWorldHint=True" )

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Vortiago/mcp-outline'

If you have feedback or need assistance with the MCP directory API, please join our Discord server