3434
3535import anyio .to_thread
3636from mcp_types import (
37+ MISSING_REQUIRED_CLIENT_CAPABILITY ,
38+ ClientCapabilities ,
39+ ElicitationCapability ,
3740 ElicitRequest ,
3841 ElicitRequestFormParams ,
3942 ElicitResult ,
43+ FormElicitationCapability ,
4044 InputRequests ,
4145 InputRequiredResult ,
4246 InputResponses ,
47+ MissingRequiredClientCapabilityErrorData ,
4348)
4449from mcp_types .version import is_version_at_least
4550from pydantic import BaseModel , ValidationError
5560from mcp .server .mcpserver .context import Context
5661from mcp .server .mcpserver .exceptions import InvalidSignature , ToolError
5762from mcp .shared ._callable_inspection import is_async_callable
63+ from mcp .shared .exceptions import MCPError
5864
5965T = TypeVar ("T" , bound = BaseModel )
6066
@@ -189,7 +195,7 @@ def _elicit_return_schema(return_annotation: Any) -> type[BaseModel] | None:
189195 for candidate in candidates :
190196 if get_origin (candidate ) is Elicit :
191197 schema = get_args (candidate )[0 ]
192- if isinstance (schema , type ) and issubclass (schema , BaseModel ): # pragma: no branch
198+ if isinstance (schema , type ) and issubclass (schema , BaseModel ):
193199 return schema
194200 return None
195201
@@ -383,7 +389,7 @@ async def resolve_arguments(
383389 # called directly builds such a `Context`, and a tool whose resolvers never elicit
384390 # must still work there. A missing version means the synchronous (non-input_required)
385391 # transport, which never reaches a server-to-client request anyway.
386- res = _Resolution (plans , tool_args , context , uses_input_required (context .protocol_version ))
392+ res = _Resolution (plans , tool_args , context , _uses_input_required (context .protocol_version ))
387393 injected : dict [str , Any ] = {}
388394 for name , (marker , wants_union ) in resolved_params .items ():
389395 try :
@@ -417,13 +423,10 @@ async def _resolve(fn: Callable[..., Any], res: _Resolution) -> ElicitationResul
417423 # `-> ... Elicit[T]`), fall through and re-run the resolver so `_elicit` can
418424 # re-validate the stored answer against the live `Elicit.schema`.
419425 if wire_key in res .state and (plan .elicit_schema is not None or res .state [wire_key ].action != "accept" ):
420- outcome = _outcome_from_state (res .state [wire_key ], plan .elicit_schema )
421- res .cache [cache_key ] = outcome
422- # Carry the restored answer forward: if a later resolver is still pending,
423- # the next round's `request_state` is built from `res.elicited`, so an
424- # earlier answer must stay there or it would be dropped and re-asked.
425- res .elicited [wire_key ] = outcome
426- return outcome
426+ outcome = _restore_outcome (res , wire_key , plan .elicit_schema )
427+ if outcome is not None :
428+ res .cache [cache_key ] = outcome
429+ return outcome
427430
428431 kwargs : dict [str , Any ] = {}
429432 dep_pending = False
@@ -470,22 +473,30 @@ async def _elicit(elicit: Elicit[Any], key: str, res: _Resolution) -> Elicitatio
470473 return await res .context .elicit (elicit .message , elicit .schema )
471474
472475 # Answered in a prior round (restored without a known schema, e.g. an unannotated
473- # resolver): re-validate the stored entry against the live `Elicit.schema`.
474- if key in res .state and key not in res .answers :
475- outcome = _outcome_from_state (res .state [key ], elicit .schema )
476- res .elicited [key ] = outcome
476+ # resolver): re-validate the stored entry against the live `Elicit.schema`. A
477+ # recorded outcome wins over a re-sent answer; an invalid entry self-deletes and
478+ # falls through to the fresh answer (or to re-asking).
479+ outcome = _restore_outcome (res , key , elicit .schema )
480+ if outcome is not None :
477481 return outcome
478482
479483 answer = res .answers .get (key )
480484 if answer is None :
485+ _require_form_elicitation (res .context , key )
481486 res .pending [key ] = _elicit_request (elicit )
482487 raise _Pending
483488 if not isinstance (answer , ElicitResult ):
484489 raise ToolError (f"Resolver { key !r} received a non-elicitation response" )
485490 if answer .action == "accept" :
486491 if answer .content is None :
487492 raise ToolError (f"Resolver { key !r} received an accepted elicitation with no content" )
488- return AcceptedElicitation (data = elicit .schema .model_validate (answer .content ))
493+ try :
494+ data = elicit .schema .model_validate (answer .content )
495+ except ValidationError as e :
496+ raise ToolError (
497+ f"Resolver { key !r} received an accepted elicitation whose content does not match the requested schema"
498+ ) from e
499+ return AcceptedElicitation (data = data )
489500 if answer .action == "decline" :
490501 return DeclinedElicitation ()
491502 return CancelledElicitation ()
@@ -511,7 +522,7 @@ def _accepted(data: Any) -> AcceptedElicitation[Any]:
511522 return AcceptedElicitation [Any ].model_construct (data = data )
512523
513524
514- def uses_input_required (protocol_version : str | None ) -> bool :
525+ def _uses_input_required (protocol_version : str | None ) -> bool :
515526 """True when this request must elicit via `InputRequiredResult` (>= 2026-07-28).
516527
517528 Older revisions still carry a standalone `elicitation/create` server-to-client
@@ -520,6 +531,31 @@ def uses_input_required(protocol_version: str | None) -> bool:
520531 return protocol_version is not None and is_version_at_least (protocol_version , _INPUT_REQUIRED_VERSION )
521532
522533
534+ def _require_form_elicitation (context : Context [Any , Any ], key : str ) -> None :
535+ """Assert the client declared form elicitation before queueing a question for it.
536+
537+ The spec forbids sending an `input_requests` entry the client has not declared a
538+ capability for. A bare `elicitation: {}` declaration (the only shape before modes
539+ existed) counts as form support; an explicit url-only declaration does not.
540+
541+ Raises:
542+ MCPError: With code `MISSING_REQUIRED_CLIENT_CAPABILITY` and a
543+ `requiredCapabilities` payload when form elicitation is not declared.
544+ """
545+ capabilities = context .client_capabilities
546+ elicitation = capabilities .elicitation if capabilities is not None else None
547+ if elicitation is not None and (elicitation .form is not None or elicitation .url is None ):
548+ return
549+ data = MissingRequiredClientCapabilityErrorData (
550+ required_capabilities = ClientCapabilities (elicitation = ElicitationCapability (form = FormElicitationCapability ()))
551+ )
552+ raise MCPError (
553+ code = MISSING_REQUIRED_CLIENT_CAPABILITY ,
554+ message = f"Client did not declare the form elicitation capability required by resolver { key !r} " ,
555+ data = data .model_dump (by_alias = True , mode = "json" , exclude_none = True ),
556+ )
557+
558+
523559def _elicit_request (elicit : Elicit [Any ]) -> ElicitRequest :
524560 """Render an `Elicit[T]` as the embedded `elicitation/create` request for `input_requests`."""
525561 json_schema = render_elicitation_schema (elicit .schema )
@@ -561,23 +597,54 @@ def _encode_state(outcomes: Mapping[str, ElicitationResult[Any]]) -> str:
561597 for path , outcome in outcomes .items ():
562598 data = outcome .data if isinstance (outcome , AcceptedElicitation ) else None
563599 if isinstance (data , BaseModel ):
564- data = data .model_dump (mode = "json" )
600+ # By alias: the stored shape must round-trip through
601+ # `schema.model_validate` on restore, which expects the alias-keyed
602+ # form the client answered with (the rendered schema is alias-keyed).
603+ data = data .model_dump (mode = "json" , by_alias = True )
565604 entries [path ] = _StateEntry (action = outcome .action , data = data )
566605 return _State (v = _STATE_VERSION , outcomes = entries ).model_dump_json ()
567606
568607
569608def _outcome_from_state (entry : _StateEntry , schema : type [BaseModel ] | None ) -> ElicitationResult [Any ]:
570- """Rebuild an `ElicitationResult` from a decoded `request_state` entry."""
609+ """Rebuild an `ElicitationResult` from a decoded `request_state` entry.
610+
611+ Raises:
612+ ValidationError: If `schema` is known and the entry's data does not
613+ validate against it.
614+ """
571615 if entry .action == "decline" :
572616 return DeclinedElicitation ()
573617 if entry .action == "cancel" :
574618 return CancelledElicitation ()
575619 data = entry .data
576- if schema is not None and isinstance ( data , dict ) :
620+ if schema is not None :
577621 data = schema .model_validate (data )
578622 return _accepted (data )
579623
580624
625+ def _restore_outcome (res : _Resolution , key : str , schema : type [BaseModel ] | None ) -> ElicitationResult [Any ] | None :
626+ """Restore `key`'s recorded outcome from a prior round, or `None` when absent.
627+
628+ `request_state` is client-trusted, so an entry whose data fails validation gets
629+ the `_decode_state` treatment - dropped as if no progress was recorded, so the
630+ question is asked again - rather than surfacing a validation error.
631+
632+ Carries a restored outcome forward in `res.elicited`: if a later resolver is
633+ still pending, the next round's `request_state` is built from `res.elicited`,
634+ so an earlier answer must stay there or it would be dropped and re-asked.
635+ """
636+ entry = res .state .get (key )
637+ if entry is None :
638+ return None
639+ try :
640+ outcome = _outcome_from_state (entry , schema )
641+ except ValidationError :
642+ del res .state [key ]
643+ return None
644+ res .elicited [key ] = outcome
645+ return outcome
646+
647+
581648__all__ = [
582649 "Resolve" ,
583650 "Elicit" ,
0 commit comments