Spaces:
Running
Running
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)
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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)
|
| 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 |
-
|
|
|
|
| 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
|