mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Merge pull request #983 from jlowin/elicit-accept
Support implicit Elicitation acceptance
This commit is contained in:
commit
44f6101606
3 changed files with 39 additions and 6 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue