Merge pull request #983 from jlowin/elicit-accept

Support implicit Elicitation acceptance
This commit is contained in:
Jeremiah Lowin 2025-06-28 20:21:32 -04:00 committed by GitHub
commit 44f6101606
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 39 additions and 6 deletions

View file

@ -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:
<Card icon="code" title="ElicitResult Structure">
<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
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",

View file

@ -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:

View file

@ -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