1+ """Tests for tool annotations in low-level server."""
2+
3+ import anyio
4+ import pytest
5+
6+ from mcp .client .session import ClientSession
7+ from mcp .server import Server
8+ from mcp .server .lowlevel import NotificationOptions
9+ from mcp .server .models import InitializationOptions
10+ from mcp .server .session import ServerSession
11+ from mcp .shared .session import RequestResponder
12+ from mcp .types import (
13+ Tool ,
14+ ToolAnnotations ,
15+ JSONRPCMessage ,
16+ ListToolsRequest ,
17+ ListToolsResult ,
18+ InitializeRequestParams ,
19+ Implementation ,
20+ ClientCapabilities ,
21+ ServerRequest ,
22+ ClientResult ,
23+ ServerNotification ,
24+ )
25+
26+
27+ @pytest .mark .anyio
28+ async def test_lowlevel_server_tool_annotations ():
29+ """Test that tool annotations work in low-level server."""
30+ server = Server ("test" )
31+
32+ # Create a tool with annotations
33+ @server .list_tools ()
34+ async def list_tools ():
35+ return [
36+ Tool (
37+ name = "echo" ,
38+ description = "Echo a message back" ,
39+ inputSchema = {
40+ "type" : "object" ,
41+ "properties" : {
42+ "message" : {"type" : "string" },
43+ },
44+ "required" : ["message" ],
45+ },
46+ annotations = ToolAnnotations (
47+ title = "Echo Tool" ,
48+ readOnlyHint = True ,
49+ ),
50+ )
51+ ]
52+
53+ server_to_client_send , server_to_client_receive = anyio .create_memory_object_stream [
54+ JSONRPCMessage
55+ ](10 )
56+ client_to_server_send , client_to_server_receive = anyio .create_memory_object_stream [
57+ JSONRPCMessage
58+ ](10 )
59+
60+ # Track results for assertion
61+ tools_result = None
62+
63+ # Message handler for client
64+ async def message_handler (
65+ message : RequestResponder [ServerRequest , ClientResult ] | ServerNotification | Exception ,
66+ ) -> None :
67+ nonlocal tools_result
68+ if isinstance (message , Exception ):
69+ raise message
70+ if isinstance (message , RequestResponder ):
71+ result = message .message .result
72+ if isinstance (result , dict ) and "tools" in result :
73+ tools_result = ListToolsResult .model_validate (result )
74+
75+ # Server task
76+ async def run_server ():
77+ async with ServerSession (
78+ client_to_server_receive ,
79+ server_to_client_send ,
80+ InitializationOptions (
81+ server_name = "test-server" ,
82+ server_version = "1.0.0" ,
83+ capabilities = server .get_capabilities (
84+ notification_options = NotificationOptions (),
85+ experimental_capabilities = {},
86+ ),
87+ ),
88+ ) as server_session :
89+ async with anyio .create_task_group () as tg :
90+
91+ async def handle_messages ():
92+ async for message in server_session .incoming_messages :
93+ await server ._handle_message (
94+ message , server_session , {}, False
95+ )
96+
97+ tg .start_soon (handle_messages )
98+ await anyio .sleep_forever ()
99+
100+ # Run the test
101+ async with anyio .create_task_group () as tg :
102+ tg .start_soon (run_server )
103+
104+ async with ClientSession (
105+ server_to_client_receive ,
106+ client_to_server_send ,
107+ message_handler = message_handler ,
108+ ) as client_session :
109+ # Initialize the session
110+ await client_session .initialize ()
111+
112+ # List tools
113+ tools_result = await client_session .list_tools ()
114+
115+
116+ # Cancel the server task
117+ tg .cancel_scope .cancel ()
118+
119+ # Verify results
120+ assert tools_result is not None
121+ assert len (tools_result .tools ) == 1
122+ assert tools_result .tools [0 ].name == "echo"
123+ assert tools_result .tools [0 ].annotations is not None
124+ assert tools_result .tools [0 ].annotations .title == "Echo Tool"
125+ assert tools_result .tools [0 ].annotations .readOnlyHint is True
0 commit comments