88 DeclinedElicitation ,
99)
1010from pydantic import BaseModel
11+ from pydantic .json_schema import GenerateJsonSchema , JsonSchemaValue
12+ from pydantic_core import core_schema
1113
1214from fastmcp .utilities .json_schema import compress_schema
1315from fastmcp .utilities .logging import get_logger
2628T = TypeVar ("T" )
2729
2830
31+ class ElicitationJsonSchema (GenerateJsonSchema ):
32+ """Custom JSON schema generator for MCP elicitation that always inlines enums.
33+
34+ MCP elicitation requires inline enum schemas without $ref/$defs references.
35+ This generator ensures enums are always generated inline for compatibility.
36+ Optionally adds enumNames for better UI display when available.
37+ """
38+
39+ def generate_inner (self , schema : core_schema .CoreSchema ) -> JsonSchemaValue :
40+ """Override to prevent ref generation for enums."""
41+ # For enum schemas, bypass the ref mechanism entirely
42+ if schema ["type" ] == "enum" :
43+ # Directly call our custom enum_schema without going through handler
44+ # This prevents the ref/defs mechanism from being invoked
45+ return self .enum_schema (schema )
46+ # For all other types, use the default implementation
47+ return super ().generate_inner (schema )
48+
49+ def enum_schema (self , schema : core_schema .EnumSchema ) -> JsonSchemaValue :
50+ """Generate inline enum schema with optional enumNames for better UI.
51+
52+ If enum members have a _display_name_ attribute or custom __str__,
53+ we'll include enumNames for better UI representation.
54+ """
55+ # Get the base schema from parent
56+ result = super ().enum_schema (schema )
57+
58+ # Try to add enumNames if the enum has display-friendly names
59+ enum_cls = schema .get ("cls" )
60+ if enum_cls :
61+ members = schema .get ("members" , [])
62+ enum_names = []
63+ has_custom_names = False
64+
65+ for member in members :
66+ # Check if member has a custom display name attribute
67+ if hasattr (member , "_display_name_" ):
68+ enum_names .append (member ._display_name_ )
69+ has_custom_names = True
70+ # Or use the member name with better formatting
71+ else :
72+ # Convert SNAKE_CASE to Title Case for display
73+ display_name = member .name .replace ("_" , " " ).title ()
74+ enum_names .append (display_name )
75+ if display_name != member .value :
76+ has_custom_names = True
77+
78+ # Only add enumNames if they differ from the values
79+ if has_custom_names :
80+ result ["enumNames" ] = enum_names
81+
82+ return result
83+
84+
2985# we can't use the low-level AcceptedElicitation because it only works with BaseModels
3086class AcceptedElicitation (BaseModel , Generic [T ]):
3187 """Result when user accepts the elicitation."""
@@ -46,7 +102,10 @@ def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
46102 response_type: The type of the response
47103 """
48104
49- schema = get_cached_typeadapter (response_type ).json_schema ()
105+ # Use custom schema generator that inlines enums for MCP compatibility
106+ schema = get_cached_typeadapter (response_type ).json_schema (
107+ schema_generator = ElicitationJsonSchema
108+ )
50109 schema = compress_schema (schema )
51110
52111 # Validate the schema to ensure it follows MCP elicitation requirements
0 commit comments