Jeremiah Lowin commited on
Commit
f1f97d2
·
1 Parent(s): 27260c2

Support implicit Elicitation acceptance

Browse files
docs/clients/elicitation.mdx CHANGED
@@ -35,7 +35,7 @@ Provide an `elicitation_handler` function when creating the client. FastMCP auto
35
  from fastmcp import Client
36
  from fastmcp.client.elicitation import ElicitResult
37
 
38
- async def elicitation_handler(message: str, response_type: type, params, context) -> ElicitResult:
39
  # Present the message to the user and collect input
40
  user_input = input(f"{message}: ")
41
 
@@ -43,7 +43,11 @@ async def elicitation_handler(message: str, response_type: type, params, context
43
  # FastMCP converted the JSON schema to this Python type for you
44
  response_data = response_type(value=user_input)
45
 
46
- return ElicitResult(action="accept", content=response_data)
 
 
 
 
47
 
48
  client = Client(
49
  "my_mcp_server.py",
@@ -75,7 +79,7 @@ The elicitation handler receives four parameters:
75
 
76
  ### Response Actions
77
 
78
- The handler must return an `ElicitResult` object that includes both an action and (when accepted) the user's input:
79
 
80
  <Card icon="code" title="ElicitResult Structure">
81
  <ResponseField name="action" type="Literal['accept', 'decline', 'cancel']">
@@ -98,18 +102,20 @@ The handler must return an `ElicitResult` object that includes both an action an
98
  from fastmcp import Client
99
  from fastmcp.client.elicitation import ElicitResult
100
 
101
- async def basic_elicitation_handler(message: str, response_type: type, params, context) -> ElicitResult:
102
  print(f"Server asks: {message}")
103
 
104
  # Simple text input for demonstration
105
  user_response = input("Your response: ")
106
 
107
  if not user_response:
 
108
  return ElicitResult(action="decline")
109
 
110
  # Use the response_type dataclass to create a properly structured response
111
  # FastMCP handles the conversion from JSON schema to Python type
112
- return ElicitResult(action="accept", content=response_type(value=user_response))
 
113
 
114
  client = Client(
115
  "my_mcp_server.py",
 
35
  from fastmcp import Client
36
  from fastmcp.client.elicitation import ElicitResult
37
 
38
+ async def elicitation_handler(message: str, response_type: type, params, context):
39
  # Present the message to the user and collect input
40
  user_input = input(f"{message}: ")
41
 
 
43
  # FastMCP converted the JSON schema to this Python type for you
44
  response_data = response_type(value=user_input)
45
 
46
+ # You can return data directly - FastMCP will implicitly accept the elicitation
47
+ return response_data
48
+
49
+ # Or explicitly return an ElicitResult for more control
50
+ # return ElicitResult(action="accept", content=response_data)
51
 
52
  client = Client(
53
  "my_mcp_server.py",
 
79
 
80
  ### Response Actions
81
 
82
+ The handler can return data directly (which implicitly accepts the elicitation) or an `ElicitResult` object for more control over the response action:
83
 
84
  <Card icon="code" title="ElicitResult Structure">
85
  <ResponseField name="action" type="Literal['accept', 'decline', 'cancel']">
 
102
  from fastmcp import Client
103
  from fastmcp.client.elicitation import ElicitResult
104
 
105
+ async def basic_elicitation_handler(message: str, response_type: type, params, context):
106
  print(f"Server asks: {message}")
107
 
108
  # Simple text input for demonstration
109
  user_response = input("Your response: ")
110
 
111
  if not user_response:
112
+ # For non-acceptance, use ElicitResult explicitly
113
  return ElicitResult(action="decline")
114
 
115
  # Use the response_type dataclass to create a properly structured response
116
  # FastMCP handles the conversion from JSON schema to Python type
117
+ # Return data directly - FastMCP will implicitly accept the elicitation
118
+ return response_type(value=user_response)
119
 
120
  client = Client(
121
  "my_mcp_server.py",
src/fastmcp/client/elicitation.py CHANGED
@@ -29,7 +29,7 @@ ElicitationHandler: TypeAlias = Callable[
29
  ElicitRequestParams,
30
  RequestContext[ClientSession, LifespanContextT],
31
  ],
32
- Awaitable[ElicitResult[T | dict[str, Any]]],
33
  ]
34
 
35
 
@@ -46,6 +46,9 @@ def create_elicitation_callback(
46
  result = await elicitation_handler(
47
  params.message, response_type, params, context
48
  )
 
 
 
49
  content = to_jsonable_python(result.content)
50
  return MCPElicitResult(**result.model_dump() | {"content": content})
51
  except Exception as e:
 
29
  ElicitRequestParams,
30
  RequestContext[ClientSession, LifespanContextT],
31
  ],
32
+ Awaitable[T | dict[str, Any] | ElicitResult[T | dict[str, Any]]],
33
  ]
34
 
35
 
 
46
  result = await elicitation_handler(
47
  params.message, response_type, params, context
48
  )
49
+ # if the user returns data, we assume they've accepted the elicitation
50
+ if not isinstance(result, ElicitResult):
51
+ result = ElicitResult(action="accept", content=result)
52
  content = to_jsonable_python(result.content)
53
  return MCPElicitResult(**result.model_dump() | {"content": content})
54
  except Exception as e:
tests/client/test_streamable_http.py CHANGED
@@ -28,6 +28,15 @@ def fastmcp_server():
28
  """Greet someone by name."""
29
  return f"Hello, {name}!"
30
 
 
 
 
 
 
 
 
 
 
31
  # Add a second tool
32
  @server.tool
33
  def add(a: int, b: int) -> int:
@@ -170,6 +179,21 @@ async def test_greet_with_progress_tool(streamable_http_server: str):
170
  progress_handler.assert_called_once_with(0.5, 1.0, "Greeting in progress")
171
 
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  async def test_nested_streamable_http_server_resolves_correctly():
174
  # tests patch for
175
  # https://github.com/modelcontextprotocol/python-sdk/pull/659
 
28
  """Greet someone by name."""
29
  return f"Hello, {name}!"
30
 
31
+ @server.tool
32
+ async def elicit(ctx: Context) -> str:
33
+ """Elicit a response from the user."""
34
+ result = await ctx.elicit("What is your name?", response_type=str)
35
+ if result.action == "accept":
36
+ return f"Hello, {result.data}!"
37
+ else:
38
+ return "No name provided"
39
+
40
  # Add a second tool
41
  @server.tool
42
  def add(a: int, b: int) -> int:
 
179
  progress_handler.assert_called_once_with(0.5, 1.0, "Greeting in progress")
180
 
181
 
182
+ @pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True)
183
+ async def test_elicitation_tool(streamable_http_server: str):
184
+ """Test calling the elicitation tool in both stateless and stateful modes."""
185
+
186
+ async def elicitation_handler(message, response_type, params, ctx):
187
+ return {"value": "Alice"}
188
+
189
+ async with Client(
190
+ transport=StreamableHttpTransport(streamable_http_server),
191
+ elicitation_handler=elicitation_handler,
192
+ ) as client:
193
+ result = await client.call_tool("greet_with_progress", {"name": "Alice"})
194
+ assert result.data == "Hello, Alice!"
195
+
196
+
197
  async def test_nested_streamable_http_server_resolves_correctly():
198
  # tests patch for
199
  # https://github.com/modelcontextprotocol/python-sdk/pull/659