55
66from mcp import types
77from mcp .client .session import ClientSession
8- from mcp .server .elicitation import AcceptedUrlElicitation , DeclinedElicitation
8+ from mcp .server .elicitation import CancelledElicitation , DeclinedElicitation
99from mcp .server .fastmcp import Context , FastMCP
1010from mcp .server .session import ServerSession
1111from mcp .shared .context import RequestContext
@@ -25,13 +25,8 @@ async def request_api_key(ctx: Context[ServerSession, None]) -> str:
2525 url = "https://example.com/api_key_setup" ,
2626 elicitation_id = "test-elicitation-001" ,
2727 )
28-
29- if result .action == "accept" :
30- return "User consented to navigate to URL"
31- elif result .action == "decline" :
32- return "User declined"
33- else :
34- return "User cancelled"
28+ # Test only checks accept path
29+ return f"User { result .action } "
3530
3631 # Create elicitation callback that accepts URL mode
3732 async def elicitation_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
@@ -49,7 +44,7 @@ async def elicitation_callback(context: RequestContext[ClientSession, None], par
4944 result = await client_session .call_tool ("request_api_key" , {})
5045 assert len (result .content ) == 1
5146 assert isinstance (result .content [0 ], TextContent )
52- assert result .content [0 ].text == "User consented to navigate to URL "
47+ assert result .content [0 ].text == "User accept "
5348
5449
5550@pytest .mark .anyio
@@ -64,13 +59,8 @@ async def oauth_flow(ctx: Context[ServerSession, None]) -> str:
6459 url = "https://example.com/oauth/authorize" ,
6560 elicitation_id = "oauth-001" ,
6661 )
67-
68- if result .action == "accept" :
69- return "User consented"
70- elif result .action == "decline" :
71- return "User declined authorization"
72- else :
73- return "User cancelled"
62+ # Test only checks decline path
63+ return f"User { result .action } authorization"
7464
7565 async def elicitation_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
7666 assert params .mode == "url"
@@ -84,7 +74,7 @@ async def elicitation_callback(context: RequestContext[ClientSession, None], par
8474 result = await client_session .call_tool ("oauth_flow" , {})
8575 assert len (result .content ) == 1
8676 assert isinstance (result .content [0 ], TextContent )
87- assert result .content [0 ].text == "User declined authorization"
77+ assert result .content [0 ].text == "User decline authorization"
8878
8979
9080@pytest .mark .anyio
@@ -99,13 +89,8 @@ async def payment_flow(ctx: Context[ServerSession, None]) -> str:
9989 url = "https://example.com/payment" ,
10090 elicitation_id = "payment-001" ,
10191 )
102-
103- if result .action == "accept" :
104- return "User consented"
105- elif result .action == "decline" :
106- return "User declined"
107- else :
108- return "User cancelled payment"
92+ # Test only checks cancel path
93+ return f"User { result .action } payment"
10994
11095 async def elicitation_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
11196 assert params .mode == "url"
@@ -119,7 +104,7 @@ async def elicitation_callback(context: RequestContext[ClientSession, None], par
119104 result = await client_session .call_tool ("payment_flow" , {})
120105 assert len (result .content ) == 1
121106 assert isinstance (result .content [0 ], TextContent )
122- assert result .content [0 ].text == "User cancelled payment"
107+ assert result .content [0 ].text == "User cancel payment"
123108
124109
125110@pytest .mark .anyio
@@ -137,14 +122,8 @@ async def setup_credentials(ctx: Context[ServerSession, None]) -> str:
137122 url = "https://example.com/setup" ,
138123 elicitation_id = "setup-001" ,
139124 )
140-
141- if isinstance (result , AcceptedUrlElicitation ):
142- return "Accepted"
143- elif isinstance (result , DeclinedElicitation ):
144- return "Declined"
145- else :
146- # Must be CancelledElicitation
147- return "Cancelled"
125+ # Test only checks accept path - return the type name
126+ return type (result ).__name__
148127
149128 async def elicitation_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
150129 return ElicitResult (action = "accept" )
@@ -157,7 +136,7 @@ async def elicitation_callback(context: RequestContext[ClientSession, None], par
157136 result = await client_session .call_tool ("setup_credentials" , {})
158137 assert len (result .content ) == 1
159138 assert isinstance (result .content [0 ], TextContent )
160- assert result .content [0 ].text == "Accepted "
139+ assert result .content [0 ].text == "AcceptedUrlElicitation "
161140
162141
163142@pytest .mark .anyio
@@ -208,11 +187,10 @@ class NameSchema(BaseModel):
208187 @mcp .tool (description = "Test form mode" )
209188 async def ask_name (ctx : Context [ServerSession , None ]) -> str :
210189 result = await ctx .elicit (message = "What is your name?" , schema = NameSchema )
211-
212- if result .action == "accept" and result .data :
213- return f"Hello, { result .data .name } !"
214- else :
215- return "No name provided"
190+ # Test only checks accept path with data
191+ assert result .action == "accept"
192+ assert result .data is not None
193+ return f"Hello, { result .data .name } !"
216194
217195 async def elicitation_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
218196 # Verify form mode parameters
@@ -281,3 +259,123 @@ async def test_url_elicitation_required_error_code():
281259 assert types .URL_ELICITATION_REQUIRED == - 32042 , (
282260 "URL_ELICITATION_REQUIRED error code must be -32042 per SEP 1036 specification"
283261 )
262+
263+
264+ @pytest .mark .anyio
265+ async def test_track_elicitation_method_exists ():
266+ """Test that track_elicitation method exists on ClientSession."""
267+ # This test just verifies the method signature and parameter handling exist
268+ # without actually calling the server (which may not implement it yet)
269+ import inspect
270+
271+ from mcp .client .session import ClientSession
272+
273+ # Verify the method exists
274+ assert hasattr (ClientSession , "track_elicitation" )
275+
276+ # Verify the method signature
277+ sig = inspect .signature (ClientSession .track_elicitation )
278+ params = list (sig .parameters .keys ())
279+ assert "elicitation_id" in params
280+ assert "progress_token" in params
281+
282+
283+ @pytest .mark .anyio
284+ async def test_elicit_url_typed_results ():
285+ """Test that elicit_url returns properly typed result objects."""
286+ from mcp .server .elicitation import elicit_url
287+
288+ mcp = FastMCP (name = "TypedResultsServer" )
289+
290+ @mcp .tool (description = "Test declined result" )
291+ async def test_decline (ctx : Context [ServerSession , None ]) -> str :
292+ result = await elicit_url (
293+ session = ctx .session ,
294+ message = "Test decline" ,
295+ url = "https://example.com/decline" ,
296+ elicitation_id = "decline-001" ,
297+ )
298+
299+ if isinstance (result , DeclinedElicitation ):
300+ return "Declined"
301+ return "Not declined"
302+
303+ @mcp .tool (description = "Test cancelled result" )
304+ async def test_cancel (ctx : Context [ServerSession , None ]) -> str :
305+ result = await elicit_url (
306+ session = ctx .session ,
307+ message = "Test cancel" ,
308+ url = "https://example.com/cancel" ,
309+ elicitation_id = "cancel-001" ,
310+ )
311+
312+ if isinstance (result , CancelledElicitation ):
313+ return "Cancelled"
314+ return "Not cancelled"
315+
316+ # Test declined result
317+ async def decline_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
318+ return ElicitResult (action = "decline" )
319+
320+ async with create_connected_server_and_client_session (
321+ mcp ._mcp_server , elicitation_callback = decline_callback
322+ ) as client_session :
323+ await client_session .initialize ()
324+
325+ result = await client_session .call_tool ("test_decline" , {})
326+ assert len (result .content ) == 1
327+ assert isinstance (result .content [0 ], TextContent )
328+ assert result .content [0 ].text == "Declined"
329+
330+ # Test cancelled result
331+ async def cancel_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
332+ return ElicitResult (action = "cancel" )
333+
334+ async with create_connected_server_and_client_session (
335+ mcp ._mcp_server , elicitation_callback = cancel_callback
336+ ) as client_session :
337+ await client_session .initialize ()
338+
339+ result = await client_session .call_tool ("test_cancel" , {})
340+ assert len (result .content ) == 1
341+ assert isinstance (result .content [0 ], TextContent )
342+ assert result .content [0 ].text == "Cancelled"
343+
344+
345+ @pytest .mark .anyio
346+ async def test_deprecated_elicit_method ():
347+ """Test the deprecated elicit() method for backward compatibility."""
348+ from pydantic import BaseModel , Field
349+
350+ mcp = FastMCP (name = "DeprecatedElicitServer" )
351+
352+ class EmailSchema (BaseModel ):
353+ email : str = Field (description = "Email address" )
354+
355+ @mcp .tool (description = "Test deprecated elicit method" )
356+ async def use_deprecated_elicit (ctx : Context [ServerSession , None ]) -> str :
357+ # Use the deprecated elicit() method which should call elicit_form()
358+ result = await ctx .session .elicit (
359+ message = "Enter your email" ,
360+ requestedSchema = EmailSchema .model_json_schema (),
361+ )
362+
363+ if result .action == "accept" and result .content :
364+ return f"Email: { result .content .get ('email' , 'none' )} "
365+ return "No email provided"
366+
367+ async def elicitation_callback (context : RequestContext [ClientSession , None ], params : ElicitRequestParams ):
368+ # Verify this is form mode
369+ assert params .mode == "form"
370+ assert params .requestedSchema is not None
371+ return ElicitResult (
action = "accept" ,
content = {
"email" :
"[email protected] " })
372+
373+ async with create_connected_server_and_client_session (
374+ mcp ._mcp_server , elicitation_callback = elicitation_callback
375+ ) as client_session :
376+ await client_session .initialize ()
377+
378+ result = await client_session .call_tool ("use_deprecated_elicit" , {})
379+ assert len (result .content ) == 1
380+ assert isinstance (result .content [0 ], TextContent )
381+ assert result .
content [
0 ].
text == "Email: [email protected] "
0 commit comments