diff --git a/docs/handlers/elicitation.md b/docs/handlers/elicitation.md index 3f3f5a6c0..05fa5c263 100644 --- a/docs/handlers/elicitation.md +++ b/docs/handlers/elicitation.md @@ -124,6 +124,24 @@ Some things must not go through the model or the client: credentials, card numbe Look at the second tool. When your server learns the out-of-band flow finished (a webhook, a poll; here it's modelled as a second tool), `ctx.session.send_elicit_complete(...)` sends `notifications/elicitation/complete` with the same `elicitation_id`. That is how the client knows it can stop showing *"waiting for payment..."*. Without it, the client can only guess. +## Ask only in a mode the client supports + +A client can declare one mode without the other - a terminal that renders a form but has no browser to open a URL, or a kiosk that can only open a URL. `ctx.session.check_client_capability` reads the `form` / `url` sub-capabilities, so a tool can pick a mode the client actually supports before it asks: + +```python +from mcp_types import ClientCapabilities, ElicitationCapability, FormElicitationCapability + + +async def book_table(ctx: Context) -> str: + wants_form = ClientCapabilities(elicitation=ElicitationCapability(form=FormElicitationCapability())) + if not ctx.session.check_client_capability(wants_form): + return "This client can't render a form; send a URL or return a default instead." + result = await ctx.elicit("Which date?", schema=AlternativeDate) + return "booked" if result.action == "accept" else "no change" +``` + +A bare `ElicitationCapability()` (no mode set) matches any client that supports elicitation at all, so name a mode only when you need that specific one. This is the same *"what if I can't ask?"* design the client-side check below calls out - now decided per mode. + ## The client side Servers ask. Clients answer by passing an **`elicitation_callback`** to `Client(...)`: diff --git a/src/mcp/server/connection.py b/src/mcp/server/connection.py index 8cb7dc421..68215d78b 100644 --- a/src/mcp/server/connection.py +++ b/src/mcp/server/connection.py @@ -388,8 +388,13 @@ def check_capability(self, capability: ClientCapabilities) -> bool: return False if capability.sampling.tools is not None and have.sampling.tools is None: return False - if capability.elicitation is not None and have.elicitation is None: - return False + if capability.elicitation is not None: + if have.elicitation is None: + return False + if capability.elicitation.form is not None and have.elicitation.form is None: + return False + if capability.elicitation.url is not None and have.elicitation.url is None: + return False if capability.experimental is not None: if have.experimental is None: return False diff --git a/tests/server/test_connection.py b/tests/server/test_connection.py index d448905a9..2ca5467a9 100644 --- a/tests/server/test_connection.py +++ b/tests/server/test_connection.py @@ -21,6 +21,7 @@ CreateMessageRequestParams, ElicitationCapability, EmptyResult, + FormElicitationCapability, Implementation, ListRootsRequest, ListRootsResult, @@ -31,6 +32,7 @@ SamplingCapability, SamplingContextCapability, SamplingToolsCapability, + UrlElicitationCapability, ) from mcp_types.version import LATEST_HANDSHAKE_VERSION, LATEST_MODERN_VERSION from pydantic import BaseModel, ValidationError @@ -364,6 +366,35 @@ def test_connection_check_capability_false_when_no_client_params_recorded(): (ClientCapabilities(experimental={"a": {}}), ClientCapabilities(experimental={"b": {}}), False), (ClientCapabilities(experimental={"a": {"x": 1}}), ClientCapabilities(experimental={"a": {"x": 2}}), False), (ClientCapabilities(experimental={"a": {}}), ClientCapabilities(experimental={"a": {}}), True), + (ClientCapabilities(elicitation=None), ClientCapabilities(elicitation=ElicitationCapability()), False), + # The client offers only URL-mode elicitation, but form mode is requested. + ( + ClientCapabilities(elicitation=ElicitationCapability(url=UrlElicitationCapability())), + ClientCapabilities(elicitation=ElicitationCapability(form=FormElicitationCapability())), + False, + ), + # The client offers only form-mode elicitation, but URL mode is requested. + ( + ClientCapabilities(elicitation=ElicitationCapability(form=FormElicitationCapability())), + ClientCapabilities(elicitation=ElicitationCapability(url=UrlElicitationCapability())), + False, + ), + ( + ClientCapabilities(elicitation=ElicitationCapability(form=FormElicitationCapability())), + ClientCapabilities(elicitation=ElicitationCapability(form=FormElicitationCapability())), + True, + ), + ( + ClientCapabilities(elicitation=ElicitationCapability(url=UrlElicitationCapability())), + ClientCapabilities(elicitation=ElicitationCapability(url=UrlElicitationCapability())), + True, + ), + # A bare elicitation request (no sub-capability) is satisfied by any elicitation support. + ( + ClientCapabilities(elicitation=ElicitationCapability(url=UrlElicitationCapability())), + ClientCapabilities(elicitation=ElicitationCapability()), + True, + ), ], ) def test_check_capability_per_field_branches(have: ClientCapabilities, want: ClientCapabilities, expected: bool):