diff --git a/docs/clients/elicitation.mdx b/docs/clients/elicitation.mdx index 5fb0e056b..2b639e352 100644 --- a/docs/clients/elicitation.mdx +++ b/docs/clients/elicitation.mdx @@ -35,7 +35,7 @@ Provide an `elicitation_handler` function when creating the client. FastMCP auto from fastmcp import Client from fastmcp.client.elicitation import ElicitResult -async def elicitation_handler(message: str, response_type: type, params, context) -> ElicitResult: +async def elicitation_handler(message: str, response_type: type, params, context): # Present the message to the user and collect input user_input = input(f"{message}: ") @@ -43,7 +43,11 @@ async def elicitation_handler(message: str, response_type: type, params, context # FastMCP converted the JSON schema to this Python type for you response_data = response_type(value=user_input) - return ElicitResult(action="accept", content=response_data) + # You can return data directly - FastMCP will implicitly accept the elicitation + return response_data + + # Or explicitly return an ElicitResult for more control + # return ElicitResult(action="accept", content=response_data) client = Client( "my_mcp_server.py", @@ -75,7 +79,7 @@ The elicitation handler receives four parameters: ### Response Actions -The handler must return an `ElicitResult` object that includes both an action and (when accepted) the user's input: +The handler can return data directly (which implicitly accepts the elicitation) or an `ElicitResult` object for more control over the response action: @@ -98,18 +102,20 @@ The handler must return an `ElicitResult` object that includes both an action an from fastmcp import Client from fastmcp.client.elicitation import ElicitResult -async def basic_elicitation_handler(message: str, response_type: type, params, context) -> ElicitResult: +async def basic_elicitation_handler(message: str, response_type: type, params, context): print(f"Server asks: {message}") # Simple text input for demonstration user_response = input("Your response: ") if not user_response: + # For non-acceptance, use ElicitResult explicitly return ElicitResult(action="decline") # Use the response_type dataclass to create a properly structured response # FastMCP handles the conversion from JSON schema to Python type - return ElicitResult(action="accept", content=response_type(value=user_response)) + # Return data directly - FastMCP will implicitly accept the elicitation + return response_type(value=user_response) client = Client( "my_mcp_server.py", diff --git a/src/fastmcp/client/elicitation.py b/src/fastmcp/client/elicitation.py index b5d8df873..e5f20d9f7 100644 --- a/src/fastmcp/client/elicitation.py +++ b/src/fastmcp/client/elicitation.py @@ -29,7 +29,7 @@ ElicitationHandler: TypeAlias = Callable[ ElicitRequestParams, RequestContext[ClientSession, LifespanContextT], ], - Awaitable[ElicitResult[T | dict[str, Any]]], + Awaitable[T | dict[str, Any] | ElicitResult[T | dict[str, Any]]], ] @@ -46,6 +46,9 @@ def create_elicitation_callback( result = await elicitation_handler( params.message, response_type, params, context ) + # if the user returns data, we assume they've accepted the elicitation + if not isinstance(result, ElicitResult): + result = ElicitResult(action="accept", content=result) content = to_jsonable_python(result.content) return MCPElicitResult(**result.model_dump() | {"content": content}) except Exception as e: diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index 7bc7b4e7d..f27f99399 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -28,6 +28,15 @@ def fastmcp_server(): """Greet someone by name.""" return f"Hello, {name}!" + @server.tool + async def elicit(ctx: Context) -> str: + """Elicit a response from the user.""" + result = await ctx.elicit("What is your name?", response_type=str) + if result.action == "accept": + return f"You said your name was: {result.data}!" + else: + return "No name provided" + # Add a second tool @server.tool def add(a: int, b: int) -> int: @@ -170,6 +179,21 @@ async def test_greet_with_progress_tool(streamable_http_server: str): progress_handler.assert_called_once_with(0.5, 1.0, "Greeting in progress") +@pytest.mark.parametrize("streamable_http_server", [True, False], indirect=True) +async def test_elicitation_tool(streamable_http_server: str): + """Test calling the elicitation tool in both stateless and stateful modes.""" + + async def elicitation_handler(message, response_type, params, ctx): + return {"value": "Alice"} + + async with Client( + transport=StreamableHttpTransport(streamable_http_server), + elicitation_handler=elicitation_handler, + ) as client: + result = await client.call_tool("elicit") + assert result.data == "You said your name was: Alice!" + + async def test_nested_streamable_http_server_resolves_correctly(): # tests patch for # https://github.com/modelcontextprotocol/python-sdk/pull/659