Jeremiah Lowin commited on
Commit
1f71f65
·
1 Parent(s): 067cea2

Handler must be async; add to sessionkwargs

Browse files
src/fastmcp/client/client.py CHANGED
@@ -252,7 +252,9 @@ class Client(Generic[ClientTransportT]):
252
  sampling_callback
253
  )
254
 
255
- def set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None:
 
 
256
  """Set the elicitation callback for the client."""
257
  self._session_kwargs["elicitation_callback"] = create_elicitation_callback(
258
  elicitation_callback
 
252
  sampling_callback
253
  )
254
 
255
+ def set_elicitation_callback(
256
+ self, elicitation_callback: ElicitationHandler
257
+ ) -> None:
258
  """Set the elicitation callback for the client."""
259
  self._session_kwargs["elicitation_callback"] = create_elicitation_callback(
260
  elicitation_callback
src/fastmcp/client/elicitation.py CHANGED
@@ -1,6 +1,5 @@
1
  from __future__ import annotations
2
 
3
- import inspect
4
  from collections.abc import Awaitable, Callable
5
  from typing import Any, TypeAlias
6
 
@@ -19,7 +18,7 @@ ElicitationHandler: TypeAlias = Callable[
19
  dict[str, Any], # requested_schema
20
  RequestContext[ClientSession, LifespanContextT],
21
  ],
22
- ElicitResult | Awaitable[ElicitResult],
23
  ]
24
 
25
 
@@ -31,12 +30,9 @@ def create_elicitation_callback(
31
  params: ElicitRequestParams,
32
  ) -> ElicitResult | mcp.types.ErrorData:
33
  try:
34
- result = elicitation_handler(
35
  params.message, params.requestedSchema, context
36
  )
37
- if inspect.isawaitable(result):
38
- result = await result
39
-
40
  return result
41
  except Exception as e:
42
  return mcp.types.ErrorData(
 
1
  from __future__ import annotations
2
 
 
3
  from collections.abc import Awaitable, Callable
4
  from typing import Any, TypeAlias
5
 
 
18
  dict[str, Any], # requested_schema
19
  RequestContext[ClientSession, LifespanContextT],
20
  ],
21
+ Awaitable[ElicitResult],
22
  ]
23
 
24
 
 
30
  params: ElicitRequestParams,
31
  ) -> ElicitResult | mcp.types.ErrorData:
32
  try:
33
+ result = await elicitation_handler(
34
  params.message, params.requestedSchema, context
35
  )
 
 
 
36
  return result
37
  except Exception as e:
38
  return mcp.types.ErrorData(
src/fastmcp/client/transports.py CHANGED
@@ -15,7 +15,13 @@ import anyio
15
  import httpx
16
  import mcp.types
17
  from mcp import ClientSession, StdioServerParameters
18
- from mcp.client.session import ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
 
 
 
 
 
 
19
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
20
  from mcp.shared.memory import create_client_server_memory_streams
21
  from pydantic import AnyUrl
@@ -56,6 +62,7 @@ class SessionKwargs(TypedDict, total=False):
56
  sampling_callback: SamplingFnT | None
57
  list_roots_callback: ListRootsFnT | None
58
  logging_callback: LoggingFnT | None
 
59
  message_handler: MessageHandlerFnT | None
60
  client_info: mcp.types.Implementation | None
61
 
 
15
  import httpx
16
  import mcp.types
17
  from mcp import ClientSession, StdioServerParameters
18
+ from mcp.client.session import (
19
+ ElicitationFnT,
20
+ ListRootsFnT,
21
+ LoggingFnT,
22
+ MessageHandlerFnT,
23
+ SamplingFnT,
24
+ )
25
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
26
  from mcp.shared.memory import create_client_server_memory_streams
27
  from pydantic import AnyUrl
 
62
  sampling_callback: SamplingFnT | None
63
  list_roots_callback: ListRootsFnT | None
64
  logging_callback: LoggingFnT | None
65
+ elicitation_callback: ElicitationFnT | None
66
  message_handler: MessageHandlerFnT | None
67
  client_info: mcp.types.Implementation | None
68
 
tests/client/test_elicitation.py CHANGED
@@ -5,6 +5,7 @@ from mcp.types import ElicitResult
5
 
6
  from fastmcp import Context, FastMCP
7
  from fastmcp.client.client import Client
 
8
  from fastmcp.server.elicitation import (
9
  AcceptedElicitation,
10
  CancelledElicitation,
@@ -38,6 +39,14 @@ def fastmcp_server():
38
  return mcp
39
 
40
 
 
 
 
 
 
 
 
 
41
  async def test_elicitation_accept_content(fastmcp_server):
42
  """Test basic elicitation functionality."""
43
 
@@ -49,7 +58,7 @@ async def test_elicitation_accept_content(fastmcp_server):
49
  fastmcp_server, elicitation_handler=elicitation_handler
50
  ) as client:
51
  result = await client.call_tool("ask_for_name", {})
52
- assert result[0].text == "Hello, Alice!" # type: ignore[attr-defined]
53
 
54
 
55
  async def test_elicitation_decline(fastmcp_server):
@@ -62,7 +71,7 @@ async def test_elicitation_decline(fastmcp_server):
62
  fastmcp_server, elicitation_handler=elicitation_handler
63
  ) as client:
64
  result = await client.call_tool("ask_for_name", {})
65
- assert result[0].text == "No name provided." # type: ignore[attr-defined]
66
 
67
 
68
  async def test_default_response_type(fastmcp_server):
@@ -86,7 +95,7 @@ async def test_default_response_type(fastmcp_server):
86
 
87
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
88
  result = await client.call_tool("ask_for_color", {})
89
- assert result[0].text == "Your favorite color is blue!" # type: ignore[attr-defined]
90
 
91
 
92
  async def test_elicitation_handler_parameters():
@@ -148,7 +157,7 @@ async def test_elicitation_default_string_schema():
148
 
149
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
150
  result = await client.call_tool("ask_for_input", {})
151
- assert result[0].text == "You said: Hello world!" # type: ignore[attr-defined]
152
 
153
 
154
  async def test_elicitation_cancel_action():
@@ -172,7 +181,7 @@ async def test_elicitation_cancel_action():
172
 
173
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
174
  result = await client.call_tool("ask_for_optional_info", {})
175
- assert result[0].text == "Request was canceled" # type: ignore[attr-defined]
176
 
177
 
178
  async def test_elicitation_number_schema():
@@ -198,7 +207,7 @@ async def test_elicitation_number_schema():
198
 
199
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
200
  result = await client.call_tool("get_age", {})
201
- assert result[0].text == "You are 25 years old" # type: ignore[attr-defined]
202
 
203
 
204
  async def test_elicitation_handler_error():
@@ -219,7 +228,7 @@ async def test_elicitation_handler_error():
219
 
220
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
221
  result = await client.call_tool("failing_elicit", {})
222
- assert "Error:" in result[0].text # type: ignore[attr-defined]
223
 
224
 
225
  async def test_elicitation_multiple_calls():
@@ -272,7 +281,7 @@ async def test_elicitation_multiple_calls():
272
 
273
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
274
  result = await client.call_tool("multi_step_form", {})
275
- assert result[0].text == "Hello Bob, you are 25 years old" # type: ignore[attr-defined]
276
  assert call_count == 2
277
 
278
 
@@ -307,7 +316,7 @@ async def test_dataclass_response_type():
307
 
308
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
309
  result = await client.call_tool("get_user_info", {})
310
- assert result[0].text == "User: Alice, age: 30" # type: ignore[attr-defined]
311
 
312
 
313
  async def test_primitive_type_string():
@@ -326,7 +335,7 @@ async def test_primitive_type_string():
326
 
327
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
328
  result = await client.call_tool("test_string", {})
329
- assert result[0].text == "Got: hello" # type: ignore[attr-defined]
330
 
331
 
332
  async def test_primitive_type_int():
@@ -345,7 +354,7 @@ async def test_primitive_type_int():
345
 
346
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
347
  result = await client.call_tool("test_int", {})
348
- assert result[0].text == "Got: 42" # type: ignore[attr-defined]
349
 
350
 
351
  async def test_primitive_type_float():
@@ -364,7 +373,7 @@ async def test_primitive_type_float():
364
 
365
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
366
  result = await client.call_tool("test_float", {})
367
- assert result[0].text == "Got: 3.14" # type: ignore[attr-defined]
368
 
369
 
370
  async def test_primitive_type_bool():
@@ -383,7 +392,7 @@ async def test_primitive_type_bool():
383
 
384
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
385
  result = await client.call_tool("test_bool", {})
386
- assert result[0].text == "Got: True" # type: ignore[attr-defined]
387
 
388
 
389
  async def test_schema_validation_rejects_non_object():
@@ -458,7 +467,7 @@ async def test_pattern_matching_accept():
458
 
459
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
460
  result = await client.call_tool("pattern_match_tool", {})
461
- assert result[0].text == "Hello Alice!" # type: ignore[attr-defined]
462
 
463
 
464
  async def test_pattern_matching_decline():
@@ -482,7 +491,7 @@ async def test_pattern_matching_decline():
482
 
483
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
484
  result = await client.call_tool("pattern_match_tool", {})
485
- assert result[0].text == "You declined" # type: ignore[attr-defined]
486
 
487
 
488
  async def test_pattern_matching_cancel():
@@ -506,4 +515,4 @@ async def test_pattern_matching_cancel():
506
 
507
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
508
  result = await client.call_tool("pattern_match_tool", {})
509
- assert result[0].text == "Cancelled" # type: ignore[attr-defined]
 
5
 
6
  from fastmcp import Context, FastMCP
7
  from fastmcp.client.client import Client
8
+ from fastmcp.exceptions import ToolError
9
  from fastmcp.server.elicitation import (
10
  AcceptedElicitation,
11
  CancelledElicitation,
 
39
  return mcp
40
 
41
 
42
+ async def test_elicitation_with_no_handler(fastmcp_server):
43
+ """Test that elicitation works without a handler."""
44
+
45
+ async with Client(fastmcp_server) as client:
46
+ with pytest.raises(ToolError, match="Elicitation not supported"):
47
+ await client.call_tool("ask_for_name", {})
48
+
49
+
50
  async def test_elicitation_accept_content(fastmcp_server):
51
  """Test basic elicitation functionality."""
52
 
 
58
  fastmcp_server, elicitation_handler=elicitation_handler
59
  ) as client:
60
  result = await client.call_tool("ask_for_name", {})
61
+ assert result.data == "Hello, Alice!"
62
 
63
 
64
  async def test_elicitation_decline(fastmcp_server):
 
71
  fastmcp_server, elicitation_handler=elicitation_handler
72
  ) as client:
73
  result = await client.call_tool("ask_for_name", {})
74
+ assert result.data == "No name provided."
75
 
76
 
77
  async def test_default_response_type(fastmcp_server):
 
95
 
96
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
97
  result = await client.call_tool("ask_for_color", {})
98
+ assert result.data == "Your favorite color is blue!"
99
 
100
 
101
  async def test_elicitation_handler_parameters():
 
157
 
158
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
159
  result = await client.call_tool("ask_for_input", {})
160
+ assert result.data == "You said: Hello world!"
161
 
162
 
163
  async def test_elicitation_cancel_action():
 
181
 
182
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
183
  result = await client.call_tool("ask_for_optional_info", {})
184
+ assert result.data == "Request was canceled"
185
 
186
 
187
  async def test_elicitation_number_schema():
 
207
 
208
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
209
  result = await client.call_tool("get_age", {})
210
+ assert result.data == "You are 25 years old"
211
 
212
 
213
  async def test_elicitation_handler_error():
 
228
 
229
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
230
  result = await client.call_tool("failing_elicit", {})
231
+ assert "Error:" in result.data
232
 
233
 
234
  async def test_elicitation_multiple_calls():
 
281
 
282
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
283
  result = await client.call_tool("multi_step_form", {})
284
+ assert result.data == "Hello Bob, you are 25 years old"
285
  assert call_count == 2
286
 
287
 
 
316
 
317
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
318
  result = await client.call_tool("get_user_info", {})
319
+ assert result.data == "User: Alice, age: 30"
320
 
321
 
322
  async def test_primitive_type_string():
 
335
 
336
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
337
  result = await client.call_tool("test_string", {})
338
+ assert result.data == "Got: hello"
339
 
340
 
341
  async def test_primitive_type_int():
 
354
 
355
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
356
  result = await client.call_tool("test_int", {})
357
+ assert result.data == "Got: 42"
358
 
359
 
360
  async def test_primitive_type_float():
 
373
 
374
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
375
  result = await client.call_tool("test_float", {})
376
+ assert result.data == "Got: 3.14"
377
 
378
 
379
  async def test_primitive_type_bool():
 
392
 
393
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
394
  result = await client.call_tool("test_bool", {})
395
+ assert result.data == "Got: True"
396
 
397
 
398
  async def test_schema_validation_rejects_non_object():
 
467
 
468
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
469
  result = await client.call_tool("pattern_match_tool", {})
470
+ assert result.data == "Hello Alice!"
471
 
472
 
473
  async def test_pattern_matching_decline():
 
491
 
492
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
493
  result = await client.call_tool("pattern_match_tool", {})
494
+ assert result.data == "You declined"
495
 
496
 
497
  async def test_pattern_matching_cancel():
 
515
 
516
  async with Client(mcp, elicitation_handler=elicitation_handler) as client:
517
  result = await client.call_tool("pattern_match_tool", {})
518
+ assert result.data == "Cancelled"