SEP-1330: Elicitation Enum Schema Improvements and Standards Compliance by chughtapan · Pull Request #1246 · modelcontextprotocol/python-sdk · GitHub
Skip to content
Merged
59 changes: 59 additions & 0 deletions examples/servers/everything-server/mcp_everything_server/server.py
44 changes: 32 additions & 12 deletions src/mcp/server/elicitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from __future__ import annotations

import types
from collections.abc import Sequence
from typing import Generic, Literal, TypeVar, Union, get_args, get_origin

from pydantic import BaseModel
from pydantic.fields import FieldInfo

from mcp.server.session import ServerSession
from mcp.types import RequestId
Expand Down Expand Up @@ -43,22 +43,40 @@ class CancelledElicitation(BaseModel):
def _validate_elicitation_schema(schema: type[BaseModel]) -> None:
"""Validate that a Pydantic model only contains primitive field types."""
for field_name, field_info in schema.model_fields.items():
if not _is_primitive_field(field_info):
annotation = field_info.annotation

if annotation is None or annotation is types.NoneType: # pragma: no cover
continue
elif _is_primitive_field(annotation):
continue
elif _is_string_sequence(annotation):
continue
else:
raise TypeError(
f"Elicitation schema field '{field_name}' must be a primitive type "
f"{_ELICITATION_PRIMITIVE_TYPES} or Optional of these types. "
f"Complex types like lists, dicts, or nested models are not allowed."
f"{_ELICITATION_PRIMITIVE_TYPES}, a sequence of strings (list[str], etc.), "
f"or Optional of these types. Nested models and complex types are not allowed."
)


def _is_primitive_field(field_info: FieldInfo) -> bool:
"""Check if a field is a primitive type allowed in elicitation schemas."""
annotation = field_info.annotation
def _is_string_sequence(annotation: type) -> bool:
"""Check if annotation is a sequence of strings (list[str], Sequence[str], etc)."""
origin = get_origin(annotation)
# Check if it's a sequence-like type with str elements
if origin:
try:
if issubclass(origin, Sequence):
args = get_args(annotation)
# Should have single str type arg
return len(args) == 1 and args[0] is str
except TypeError: # pragma: no cover
# origin is not a class, so it can't be a subclass of Sequence
pass
return False

# Handle None type
if annotation is types.NoneType: # pragma: no cover
return True

def _is_primitive_field(annotation: type) -> bool:
"""Check if a field is a primitive type allowed in elicitation schemas."""
# Handle basic primitive types
if annotation in _ELICITATION_PRIMITIVE_TYPES:
return True
Expand All @@ -67,8 +85,10 @@ def _is_primitive_field(field_info: FieldInfo) -> bool:
origin = get_origin(annotation)
if origin is Union or origin is types.UnionType:
args = get_args(annotation)
# All args must be primitive types or None
return all(arg is types.NoneType or arg in _ELICITATION_PRIMITIVE_TYPES for arg in args)
# All args must be primitive types, None, or string sequences
return all(
arg is types.NoneType or arg in _ELICITATION_PRIMITIVE_TYPES or _is_string_sequence(arg) for arg in args
)

return False

Expand Down
2 changes: 1 addition & 1 deletion src/mcp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,7 +1468,7 @@ class ElicitResult(Result):
- "cancel": User dismissed without making an explicit choice
"""

content: dict[str, str | int | float | bool | None] | None = None
content: dict[str, str | int | float | bool | list[str] | None] | None = None
"""
The submitted form data, only present when action is "accept".
Contains values matching the requested schema.
Expand Down
133 changes: 130 additions & 3 deletions tests/server/fastmcp/test_elicitation.py