fastmcp/tests/client/test_elicitation.py
Jeremiah Lowin 3a9717e6be
Publish docs for v3.2.0 (#3713)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: nightcityblade <nightcityblade@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: Sumanshu Nankana <sumanshunankana@gmail.com>
Co-authored-by: Eric Robinson <ericrobinson@indeed.com>
Co-authored-by: Martim Santos <martimfasantos@gmail.com>
Co-authored-by: d 🔹 <liusway405@gmail.com>
Co-authored-by: Matthieu B <66959271+mtthidoteu@users.noreply.github.com>
Co-authored-by: Sascha Buehrle <47737812+saschabuehrle@users.noreply.github.com>
Co-authored-by: Hakancan <142545736+hkc5@users.noreply.github.com>
Co-authored-by: nightcityblade <jackchen@haloailabs.com>
Co-authored-by: Matt Hallowell <17804673+mhallo@users.noreply.github.com>
Co-authored-by: nate nowack <thrast36@gmail.com>
Co-authored-by: Bill Easton <williamseaston@gmail.com>
Co-authored-by: Marcus Shu <46469249+shulkx@users.noreply.github.com>
Co-authored-by: Rushabh Doshi <radoshi@gmail.com>
Co-authored-by: AIKAWA Shigechika <shige@aikawa.jp>
Co-authored-by: Jeremy Simon <simonjer805@gmail.com>
Co-authored-by: Miguel Miranda Dias <7780875+pandego@users.noreply.github.com>
Co-authored-by: Anthony James Padavano <padavano.anthony@gmail.com>
Co-authored-by: Mostafa Kamal <hiremostafa@gmail.com>
Fix auto-close MRE script posting comment without closing (#3386)
Fix WorkOS token scope verification bypass 🤖 Generated with Codex (#3407)
Fix initialize McpError fallthrough 🤖 Generated with Codex (#3413)
Fix transform arg collisions with passthrough params (#3431)
Fix get_* returning None when latest version is disabled (#3439)
Fix get_* returning None when latest version is disabled (#3421)
Fix server lifespan overlap teardown (#3415)
Fix $ref output schema object detection regression (#3420)
resolved annotations (#3429)
Fix async partial callables rejected by iscoroutinefunction (#3438)
Fix async partial callables rejected by iscoroutinefunction (#3423)
fix: add version to components (#3458)
fix: use intent-based flag for OIDC scope patch in load_access_token (#3465)
Fixes #3461
fix: normalize Google scope shorthands and surface valid_scopes (#3477)
fix: resolve ty 0.0.23 type-checking errors and bump pin (#3481)
fix: shield lifespan teardown from cancellation (#3480)
fix: forward custom_route endpoints from mounted servers (#3462)
fix updates _get_additional_http_routes() to traverse providers,
Fixes #3457
fix: remove hardcoded version from CLI help text (#3456)
fix: monty 0.0.8 compatibility, drop external_functions from constructor (#3468)
fix: task test teardown hanging 5s per test (#3499)
Closes #3498
fix: validate workspace path is a directory before cursor install (#3440)
Fixes #3426
fix: handle re.error from malformed URI templates in build_regex (#3501)
fix: reject empty/OIDC-only required_scopes in AzureProvider (#3503)
fix: restrict $ref resolution to local refs only (SSRF/LFI) (#3502)
fix warnings and timeouts (#3504)
close upgrade check issue when build passes (#3505)
Closes #3484
fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) (#3507)
fix: prevent path traversal in skill download (#3493)
fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy (#3492)
fix: remove unrelated transform and http.py changes from PR scope
fix: remove forced follow_redirects from httpx_client_factory calls (#3496)
fix: stop passing follow_redirects to httpx_client_factory
fix: restore follow_redirects=True for custom httpx client factories
Closes #3509
fix: CSRF double-submit cookie check in consent flow (#3519)
fix: validate server names in install commands (#3522)
fix: use raw strings for regex in pytest.raises match (#3523)
fix: reject refresh tokens used as Bearer access tokens (#3524)
fix: route ResourcesAsTools/PromptsAsTools through server middleware (#3495)
fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators (#3540)
fix: filter warnings by message in KEY_PREFIX test (#3549)
fix: suppress output schema for ToolResult subclass annotations (#3548)
fix: increase sleep duration in proxy cache tests (#3567)
fix: store absolute token expiry to prevent stale expires_in on reload (#3572)
fix: preserve tool properties named 'title' during schema compression (#3582)
Fix loopback redirect URI port matching per RFC 8252 §7.3 (#3589)
Fix app tool routing: visibility check and middleware propagation (#3591)
Fix query parameter serialization to respect OpenAPI explode/style settings (#3595)
Fix dev apps form: union types, textarea support, JSON parsing (#3597)
fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo (#3603)
fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609)
fix(docs): correct misleading stateless_http header (#3622)
fix: filesystem provider import machinery (#3626)
Closes #3625 (issues 2, 3, 6)
fix: recover StdioTransport after subprocess exits (#3630)
fix(server): preserve mounted tool task metadata (#3632)
fix: scope deprecation warning filter to FastMCPDeprecationWarning (#3649)
fix imports, add PrefabAppConfig (#3650)
fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks (#3651)
Fix blocking docs issues: chart imports, Select API, Rx consistency (#3652)
closed by default (#3657)
Fix prompt caching middleware missing wrap/unwrap round-trip (#3666)
fix: serialize object query params per OpenAPI style/explode rules (#3662)
Fixes #2857
fix: HTTP request headers not accessible in background task workers (#3631)
fix: restore HTTP headers in worker execution path for background tasks (#3681)
fix: strip discriminator after dereferencing schemas (#3682)
fix: remove stale ty:ignore directives for ty 0.0.26 (#3684)
Fix docs gaps in app provider pages (#3690)
fix: dev apps log panel UX improvements (#3698)
fix dev server empty string args (#3700)
2026-03-30 16:48:30 -04:00

660 lines
24 KiB
Python

from dataclasses import asdict, dataclass
from enum import Enum
from typing import Any, Literal, cast
import pytest
from mcp.types import ElicitRequestFormParams, ElicitRequestParams
from pydantic import BaseModel
from typing_extensions import TypedDict
from fastmcp import Context, FastMCP
from fastmcp.client.client import Client
from fastmcp.client.elicitation import ElicitResult
from fastmcp.exceptions import ToolError
from fastmcp.server.elicitation import (
AcceptedElicitation,
CancelledElicitation,
DeclinedElicitation,
validate_elicitation_json_schema,
)
from fastmcp.utilities.types import TypeAdapter
@pytest.fixture
def fastmcp_server():
mcp = FastMCP("TestServer")
@dataclass
class Person:
name: str
@mcp.tool
async def ask_for_name(context: Context) -> str:
result = await context.elicit(
message="What is your name?",
response_type=Person,
)
if result.action == "accept":
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, Person)
return f"Hello, {result.data.name}!"
else:
return "No name provided."
@mcp.tool
def simple_test() -> str:
return "Hello!"
return mcp
async def test_elicitation_with_no_handler(fastmcp_server):
"""Test that elicitation works without a handler."""
async with Client(fastmcp_server) as client:
with pytest.raises(ToolError, match="Elicitation not supported"):
await client.call_tool("ask_for_name")
async def test_elicitation_accept_content(fastmcp_server):
"""Test basic elicitation functionality."""
async def elicitation_handler(message, response_type, params, ctx):
# Mock user providing their name
return ElicitResult(action="accept", content=response_type(name="Alice"))
async with Client(
fastmcp_server, elicitation_handler=elicitation_handler
) as client:
result = await client.call_tool("ask_for_name")
assert result.data == "Hello, Alice!"
async def test_elicitation_decline(fastmcp_server):
"""Test that elicitation handler receives correct parameters."""
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="decline")
async with Client(
fastmcp_server, elicitation_handler=elicitation_handler
) as client:
result = await client.call_tool("ask_for_name")
assert result.data == "No name provided."
async def test_elicitation_handler_parameters():
"""Test that elicitation handler receives correct parameters."""
mcp = FastMCP("TestServer")
captured_params = {}
@mcp.tool
async def test_tool(context: Context) -> str:
await context.elicit(
message="Test message",
response_type=int,
)
return "done"
async def elicitation_handler(message, response_type, params, ctx):
captured_params["message"] = message
captured_params["response_type"] = str(response_type)
captured_params["params"] = params
captured_params["ctx"] = ctx
return ElicitResult(action="accept", content={"value": 42})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
await client.call_tool("test_tool", {})
assert captured_params["message"] == "Test message"
assert "ScalarElicitationType" in str(captured_params["response_type"])
assert captured_params["params"].requestedSchema == {
"properties": {"value": {"title": "Value", "type": "integer"}},
"required": ["value"],
"title": "ScalarElicitationType",
"type": "object",
}
assert captured_params["ctx"] is not None
async def test_elicitation_cancel_action():
"""Test user canceling elicitation request."""
mcp = FastMCP("TestServer")
@mcp.tool
async def ask_for_optional_info(context: Context) -> str:
result = await context.elicit(
message="Optional: What's your age?", response_type=int
)
if result.action == "cancel":
return "Request was canceled"
elif result.action == "accept":
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, int)
return f"Age: {result.data}"
else:
return "No response provided"
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="cancel")
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("ask_for_optional_info", {})
assert result.data == "Request was canceled"
class TestScalarResponseTypes:
async def test_elicitation_no_response(self):
"""Test elicitation with no response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
result = await context.elicit(message="", response_type=None)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, dict)
return cast(dict[str, Any], result.data)
async def elicitation_handler(
message, response_type, params: ElicitRequestParams, ctx
):
assert isinstance(params, ElicitRequestFormParams)
assert params.requestedSchema == {"type": "object", "properties": {}}
assert response_type is None
return ElicitResult(action="accept")
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data is None
async def test_elicitation_empty_response(self):
"""Test elicitation with empty response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
result = await context.elicit(message="", response_type=None)
assert result.action == "accept"
assert isinstance(result, AcceptedElicitation)
accepted = cast(AcceptedElicitation[dict[str, Any]], result)
assert isinstance(accepted.data, dict)
return accepted.data
async def elicitation_handler(
message, response_type, params: ElicitRequestParams, ctx
):
return ElicitResult(action="accept", content={})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data is None
async def test_elicitation_response_when_no_response_requested(self):
"""Test elicitation with no response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
result = await context.elicit(message="", response_type=None)
assert result.action == "accept"
assert isinstance(result, AcceptedElicitation)
accepted = cast(AcceptedElicitation[dict[str, Any]], result)
assert isinstance(accepted.data, dict)
return accepted.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "hello"})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
with pytest.raises(
ToolError, match="Elicitation expected an empty response"
):
await client.call_tool("my_tool", {})
async def test_elicitation_str_response(self):
"""Test elicitation with string schema."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> str:
result = await context.elicit(message="", response_type=str)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, str)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "hello"})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data == "hello"
async def test_elicitation_int_response(self):
"""Test elicitation with number schema."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> int:
result = await context.elicit(message="", response_type=int)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, int)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": 42})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data == 42
async def test_elicitation_float_response(self):
"""Test elicitation with number schema."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> float:
result = await context.elicit(message="", response_type=float)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, float)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": 3.14})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data == 3.14
async def test_elicitation_bool_response(self):
"""Test elicitation with boolean schema."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> bool:
result = await context.elicit(message="", response_type=bool)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, bool)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": True})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data is True
async def test_elicitation_literal_response(self):
"""Test elicitation with literal schema."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> Literal["x", "y"]:
# Literal types work at runtime but type checker doesn't recognize them in overloads
result = await context.elicit(message="", response_type=Literal["x", "y"]) # type: ignore[arg-type] # ty:ignore[no-matching-overload]
assert isinstance(result, AcceptedElicitation)
accepted = cast(AcceptedElicitation[Literal["x", "y"]], result)
assert isinstance(accepted.data, str)
return accepted.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "x"})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data == "x"
async def test_elicitation_enum_response(self):
"""Test elicitation with enum schema."""
mcp = FastMCP("TestServer")
class ResponseEnum(Enum):
X = "x"
Y = "y"
@mcp.tool
async def my_tool(context: Context) -> ResponseEnum:
result = await context.elicit(message="", response_type=ResponseEnum)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, ResponseEnum)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "x"})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data == "x"
async def test_elicitation_list_of_strings_response(self):
"""Test elicitation with list schema."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> str:
result = await context.elicit(message="", response_type=["x", "y"])
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, str)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "x"})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("my_tool", {})
assert result.data == "x"
async def test_elicitation_handler_error():
"""Test error handling in elicitation handler."""
mcp = FastMCP("TestServer")
@mcp.tool
async def failing_elicit(context: Context) -> str:
try:
result = await context.elicit(message="This will fail", response_type=str)
assert isinstance(result, AcceptedElicitation)
assert result.action == "accept"
return f"Got: {result.data}"
except Exception as e:
return f"Error: {str(e)}"
async def elicitation_handler(message, response_type, params, ctx):
raise ValueError("Handler failed!")
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("failing_elicit", {})
assert "Error:" in result.data
async def test_elicitation_multiple_calls():
"""Test multiple elicitation calls in sequence."""
mcp = FastMCP("TestServer")
@mcp.tool
async def multi_step_form(context: Context) -> str:
# First question
name_result = await context.elicit(
message="What's your name?", response_type=str
)
assert isinstance(name_result, AcceptedElicitation)
if name_result.action != "accept":
return "Form abandoned"
# Second question
age_result = await context.elicit(message="What's your age?", response_type=int)
assert isinstance(age_result, AcceptedElicitation)
if age_result.action != "accept":
return f"Hello {name_result.data}, form incomplete"
return f"Hello {name_result.data}, you are {age_result.data} years old"
call_count = 0
async def elicitation_handler(message, response_type, params, ctx):
nonlocal call_count
call_count += 1
if call_count == 1:
return ElicitResult(action="accept", content={"value": "Bob"})
elif call_count == 2:
return ElicitResult(action="accept", content={"value": 25})
else:
raise ValueError("Unexpected call")
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("multi_step_form", {})
assert result.data == "Hello Bob, you are 25 years old"
assert call_count == 2
@dataclass
class UserInfo:
name: str
age: int
class UserInfoTypedDict(TypedDict):
name: str
age: int
class UserInfoPydantic(BaseModel):
name: str
age: int
@pytest.mark.parametrize(
"structured_type", [UserInfo, UserInfoTypedDict, UserInfoPydantic]
)
async def test_structured_response_type(
structured_type: type[UserInfo | UserInfoTypedDict | UserInfoPydantic],
):
"""Test elicitation with dataclass response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def get_user_info(context: Context) -> str:
result = await context.elicit(
message="Please provide your information", response_type=structured_type
)
assert isinstance(result, AcceptedElicitation)
if result.action == "accept":
assert isinstance(result, AcceptedElicitation)
if isinstance(result.data, dict):
data_dict = cast(dict[str, Any], result.data)
name = data_dict.get("name")
age = data_dict.get("age")
assert name is not None
assert age is not None
return f"User: {name}, age: {age}"
else:
# result.data is a structured type (UserInfo, UserInfoTypedDict, or UserInfoPydantic)
assert hasattr(result.data, "name")
assert hasattr(result.data, "age")
return f"User: {result.data.name}, age: {result.data.age}"
return "No user info provided"
async def elicitation_handler(message, response_type, params, ctx):
# Verify we get the dataclass type
assert (
TypeAdapter(response_type).json_schema()
== TypeAdapter(structured_type).json_schema()
)
# Verify the schema has the dataclass fields (available in params)
schema = params.requestedSchema
assert schema["type"] == "object"
assert "name" in schema["properties"]
assert "age" in schema["properties"]
assert schema["properties"]["name"]["type"] == "string"
assert schema["properties"]["age"]["type"] == "integer"
return ElicitResult(action="accept", content=UserInfo(name="Alice", age=30))
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("get_user_info", {})
assert result.data == "User: Alice, age: 30"
async def test_all_primitive_field_types():
class DataEnum(Enum):
X = "x"
Y = "y"
@dataclass
class Data:
integer: int
float_: float
number: int | float
boolean: bool
string: str
constant: Literal["x"]
union: Literal["x"] | Literal["y"]
choice: Literal["x", "y"]
enum: DataEnum
mcp = FastMCP("TestServer")
@mcp.tool
async def get_data(context: Context) -> Data:
result = await context.elicit(message="Enter data", response_type=Data)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, Data)
return result.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(
action="accept",
content=Data(
integer=1,
float_=1.0,
number=1.0,
boolean=True,
string="hello",
constant="x",
union="x",
choice="x",
enum=DataEnum.X,
),
)
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("get_data", {})
# Now all literal/enum fields should be preserved as strings
result_data = asdict(result.data)
result_data_enum = result_data.pop("enum")
assert result_data_enum == "x" # Should be a string now, not an enum
assert result_data == {
"integer": 1,
"float_": 1.0,
"number": 1.0,
"boolean": True,
"string": "hello",
"constant": "x",
"union": "x",
"choice": "x",
}
class TestValidation:
async def test_schema_validation_rejects_non_object(self):
"""Test that non-object schemas are rejected."""
with pytest.raises(TypeError, match="must be an object schema"):
validate_elicitation_json_schema({"type": "string"})
async def test_schema_validation_rejects_nested_objects(self):
"""Test that nested object schemas are rejected."""
with pytest.raises(
TypeError, match="is an object, but nested objects are not allowed"
):
validate_elicitation_json_schema(
{
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {"name": {"type": "string"}},
}
},
}
)
async def test_schema_validation_rejects_arrays(self):
"""Test that non-enum array schemas are rejected."""
with pytest.raises(TypeError, match="is an array, but arrays are only allowed"):
validate_elicitation_json_schema(
{
"type": "object",
"properties": {
"users": {"type": "array", "items": {"type": "string"}}
},
}
)
class TestPatternMatching:
async def test_pattern_matching_accept(self):
"""Test pattern matching with AcceptedElicitation."""
mcp = FastMCP("TestServer")
@mcp.tool
async def pattern_match_tool(context: Context) -> str:
result = await context.elicit("Enter your name:", response_type=str)
match result:
case AcceptedElicitation(data=name):
return f"Hello {name}!"
case DeclinedElicitation():
return "You declined"
case CancelledElicitation():
return "Cancelled"
case _:
return "Unknown result"
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "Alice"})
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("pattern_match_tool", {})
assert result.data == "Hello Alice!"
async def test_pattern_matching_decline(self):
"""Test pattern matching with DeclinedElicitation."""
mcp = FastMCP("TestServer")
@mcp.tool
async def pattern_match_tool(context: Context) -> str:
result = await context.elicit("Enter your name:", response_type=str)
match result:
case AcceptedElicitation(data=name):
return f"Hello {name}!"
case DeclinedElicitation():
return "You declined"
case CancelledElicitation():
return "Cancelled"
case _:
return "Unknown result"
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="decline")
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("pattern_match_tool", {})
assert result.data == "You declined"
async def test_pattern_matching_cancel(self):
"""Test pattern matching with CancelledElicitation."""
mcp = FastMCP("TestServer")
@mcp.tool
async def pattern_match_tool(context: Context) -> str:
result = await context.elicit("Enter your name:", response_type=str)
match result:
case AcceptedElicitation(data=name):
return f"Hello {name}!"
case DeclinedElicitation():
return "You declined"
case CancelledElicitation():
return "Cancelled"
case _:
return "Unknown result"
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="cancel")
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
result = await client.call_tool("pattern_match_tool", {})
assert result.data == "Cancelled"