1414
1515import pytest
1616import uvicorn
17- from pydantic import AnyUrl
17+ from pydantic import AnyUrl , BaseModel , Field
1818from starlette .applications import Starlette
1919from starlette .requests import Request
2020
@@ -102,19 +102,15 @@ def echo(message: str) -> str:
102102 # Add a tool that uses elicitation
103103 @mcp .tool (description = "A tool that uses elicitation" )
104104 async def ask_user (prompt : str , ctx : Context ) -> str :
105- schema = {
106- "type" : "object" ,
107- "properties" : {
108- "answer" : {"type" : "string" },
109- },
110- "required" : ["answer" ],
111- }
105+ class AnswerSchema (BaseModel ):
106+ answer : str = Field (description = "The user's answer to the question" )
112107
113- response = await ctx .elicit (
114- message = f"Tool wants to ask: { prompt } " ,
115- requestedSchema = schema ,
116- )
117- return f"User answered: { response ['answer' ]} "
108+ try :
109+ result = await ctx .elicit (message = f"Tool wants to ask: { prompt } " , schema = AnswerSchema )
110+ return f"User answered: { result .answer } "
111+ except Exception as e :
112+ # Handle cancellation or decline
113+ return f"User cancelled or declined: { str (e )} "
118114
119115 # Create the SSE app
120116 app = mcp .sse_app ()
@@ -279,6 +275,47 @@ def echo_context(custom_request_id: str, ctx: Context[Any, Any, Request]) -> str
279275 context_data ["path" ] = request .url .path
280276 return json .dumps (context_data )
281277
278+ # Restaurant booking tool with elicitation
279+ @mcp .tool (description = "Book a table at a restaurant with elicitation" )
280+ async def book_restaurant (
281+ date : str ,
282+ time : str ,
283+ party_size : int ,
284+ ctx : Context ,
285+ ) -> str :
286+ """Book a table - uses elicitation if requested date is unavailable."""
287+
288+ class AlternativeDateSchema (BaseModel ):
289+ checkAlternative : bool = Field (description = "Would you like to try another date?" )
290+ alternativeDate : str = Field (
291+ default = "2024-12-26" ,
292+ description = "What date would you prefer? (YYYY-MM-DD)" ,
293+ )
294+
295+ # For testing: assume dates starting with "2024-12-25" are unavailable
296+ if date .startswith ("2024-12-25" ):
297+ # Use elicitation to ask about alternatives
298+ try :
299+ result = await ctx .elicit (
300+ message = (
301+ f"No tables available for { party_size } people on { date } "
302+ f"at { time } . Would you like to check another date?"
303+ ),
304+ schema = AlternativeDateSchema ,
305+ )
306+
307+ if result .checkAlternative :
308+ alt_date = result .alternativeDate
309+ return f"✅ Booked table for { party_size } on { alt_date } at { time } "
310+ else :
311+ return "❌ No booking made"
312+ except Exception :
313+ # User declined or cancelled
314+ return "❌ Booking cancelled"
315+ else :
316+ # Available - book directly
317+ return f"✅ Booked table for { party_size } on { date } at { time } "
318+
282319 return mcp
283320
284321
@@ -670,6 +707,22 @@ async def handle_generic_notification(self, message) -> None:
670707 await self .handle_tool_list_changed (message .root .params )
671708
672709
710+ async def create_test_elicitation_callback (context , params ):
711+ """Shared elicitation callback for tests.
712+
713+ Handles elicitation requests for restaurant booking tests.
714+ """
715+ # For restaurant booking test
716+ if "No tables available" in params .message :
717+ return ElicitResult (
718+ action = "accept" ,
719+ content = {"checkAlternative" : True , "alternativeDate" : "2024-12-26" },
720+ )
721+ else :
722+ # Default response
723+ return ElicitResult (action = "decline" )
724+
725+
673726async def call_all_mcp_features (session : ClientSession , collector : NotificationCollector ) -> None :
674727 """
675728 Test all MCP features using the provided session.
@@ -765,6 +818,21 @@ async def progress_callback(progress: float, total: float | None, message: str |
765818 assert "info" in log_levels
766819 assert "warning" in log_levels
767820
821+ # 5. Test elicitation tool
822+ # Test restaurant booking with unavailable date (triggers elicitation)
823+ booking_result = await session .call_tool (
824+ "book_restaurant" ,
825+ {
826+ "date" : "2024-12-25" , # Unavailable date to trigger elicitation
827+ "time" : "19:00" ,
828+ "party_size" : 4 ,
829+ },
830+ )
831+ assert len (booking_result .content ) == 1
832+ assert isinstance (booking_result .content [0 ], TextContent )
833+ # Should have booked the alternative date from elicitation callback
834+ assert "✅ Booked table for 4 on 2024-12-26" in booking_result .content [0 ].text
835+
768836 # Test resources
769837 # 1. Static resource
770838 resources = await session .list_resources ()
@@ -905,8 +973,6 @@ async def test_fastmcp_all_features_sse(everything_server: None, everything_serv
905973 # Create notification collector
906974 collector = NotificationCollector ()
907975
908- # Create a sampling callback that simulates an LLM
909-
910976 # Connect to the server with callbacks
911977 async with sse_client (everything_server_url + "/sse" ) as streams :
912978 # Set up message handler to capture notifications
@@ -919,6 +985,7 @@ async def message_handler(message):
919985 async with ClientSession (
920986 * streams ,
921987 sampling_callback = sampling_callback ,
988+ elicitation_callback = create_test_elicitation_callback ,
922989 message_handler = message_handler ,
923990 ) as session :
924991 # Run the common test suite
@@ -951,6 +1018,7 @@ async def message_handler(message):
9511018 read_stream ,
9521019 write_stream ,
9531020 sampling_callback = sampling_callback ,
1021+ elicitation_callback = create_test_elicitation_callback ,
9541022 message_handler = message_handler ,
9551023 ) as session :
9561024 # Run the common test suite with HTTP-specific test suffix
@@ -965,7 +1033,7 @@ async def test_elicitation_feature(server: None, server_url: str) -> None:
9651033 async def elicitation_callback (context , params ):
9661034 # Verify the elicitation parameters
9671035 if params .message == "Tool wants to ask: What is your name?" :
968- return ElicitResult (content = {"answer" : "Test User" })
1036+ return ElicitResult (content = {"answer" : "Test User" }, action = "accept" )
9691037 else :
9701038 raise ValueError ("Unexpected elicitation message" )
9711039
0 commit comments