@@ -116,7 +116,7 @@ async def tool(ctx: Context[ServerSession, None]) -> str: # pragma: no cover
116116
117117 # Test cases for invalid schemas
118118 class InvalidListSchema (BaseModel ):
119- names : list [str ] = Field (description = "List of names " )
119+ numbers : list [int ] = Field (description = "List of numbers " )
120120
121121 class NestedModel (BaseModel ):
122122 value : str
@@ -139,7 +139,7 @@ async def elicitation_callback(
139139 await client_session .initialize ()
140140
141141 # Test both invalid schemas
142- for tool_name , field_name in [("invalid_list" , "names " ), ("nested_model" , "nested" )]:
142+ for tool_name , field_name in [("invalid_list" , "numbers " ), ("nested_model" , "nested" )]:
143143 result = await client_session .call_tool (tool_name , {})
144144 assert len (result .content ) == 1
145145 assert isinstance (result .content [0 ], TextContent )
@@ -197,7 +197,7 @@ async def callback(context: RequestContext[ClientSession, None], params: ElicitR
197197 # Test invalid optional field
198198 class InvalidOptionalSchema (BaseModel ):
199199 name : str = Field (description = "Name" )
200- optional_list : list [str ] | None = Field (default = None , description = "Invalid optional list" )
200+ optional_list : list [int ] | None = Field (default = None , description = "Invalid optional list" )
201201
202202 @mcp .tool (description = "Tool with invalid optional field" )
203203 async def invalid_optional_tool (ctx : Context [ServerSession , None ]) -> str : # pragma: no cover
@@ -220,6 +220,47 @@ async def elicitation_callback(
220220 text_contains = ["Validation failed:" , "optional_list" ],
221221 )
222222
223+ # Test valid list[str] for multi-select enum
224+ class ValidMultiSelectSchema (BaseModel ):
225+ name : str = Field (description = "Name" )
226+ tags : list [str ] = Field (description = "Tags" )
227+
228+ @mcp .tool (description = "Tool with valid list[str] field" )
229+ async def valid_multiselect_tool (ctx : Context [ServerSession , None ]) -> str :
230+ result = await ctx .elicit (message = "Please provide tags" , schema = ValidMultiSelectSchema )
231+ if result .action == "accept" and result .data :
232+ return f"Name: { result .data .name } , Tags: { ', ' .join (result .data .tags )} "
233+ return f"User { result .action } " # pragma: no cover
234+
235+ async def multiselect_callback (context : RequestContext [ClientSession , Any ], params : ElicitRequestParams ):
236+ if "Please provide tags" in params .message :
237+ return ElicitResult (action = "accept" , content = {"name" : "Test" , "tags" : ["tag1" , "tag2" ]})
238+ return ElicitResult (action = "decline" ) # pragma: no cover
239+
240+ await call_tool_and_assert (mcp , multiselect_callback , "valid_multiselect_tool" , {}, "Name: Test, Tags: tag1, tag2" )
241+
242+ # Test Optional[list[str]] for optional multi-select enum
243+ class OptionalMultiSelectSchema (BaseModel ):
244+ name : str = Field (description = "Name" )
245+ tags : list [str ] | None = Field (default = None , description = "Optional tags" )
246+
247+ @mcp .tool (description = "Tool with optional list[str] field" )
248+ async def optional_multiselect_tool (ctx : Context [ServerSession , None ]) -> str :
249+ result = await ctx .elicit (message = "Please provide optional tags" , schema = OptionalMultiSelectSchema )
250+ if result .action == "accept" and result .data :
251+ tags_str = ", " .join (result .data .tags ) if result .data .tags else "none"
252+ return f"Name: { result .data .name } , Tags: { tags_str } "
253+ return f"User { result .action } " # pragma: no cover
254+
255+ async def optional_multiselect_callback (context : RequestContext [ClientSession , Any ], params : ElicitRequestParams ):
256+ if "Please provide optional tags" in params .message :
257+ return ElicitResult (action = "accept" , content = {"name" : "Test" , "tags" : ["tag1" , "tag2" ]})
258+ return ElicitResult (action = "decline" ) # pragma: no cover
259+
260+ await call_tool_and_assert (
261+ mcp , optional_multiselect_callback , "optional_multiselect_tool" , {}, "Name: Test, Tags: tag1, tag2"
262+ )
263+
223264
224265@pytest .mark .anyio
225266async def test_elicitation_with_default_values ():
@@ -274,3 +315,89 @@ async def callback_override(context: RequestContext[ClientSession, None], params
274315 await call_tool_and_assert (
275316 mcp ,
callback_override ,
"defaults_tool" , {},
"Name: John, Age: 25, Subscribe: False, Email: [email protected] " 276317 )
318+
319+
320+ @pytest .mark .anyio
321+ async def test_elicitation_with_enum_titles ():
322+ """Test elicitation with enum schemas using oneOf/anyOf for titles."""
323+ mcp = FastMCP (name = "ColorPreferencesApp" )
324+
325+ # Test single-select with titles using oneOf
326+ class FavoriteColorSchema (BaseModel ):
327+ user_name : str = Field (description = "Your name" )
328+ favorite_color : str = Field (
329+ description = "Select your favorite color" ,
330+ json_schema_extra = {
331+ "oneOf" : [
332+ {"const" : "red" , "title" : "Red" },
333+ {"const" : "green" , "title" : "Green" },
334+ {"const" : "blue" , "title" : "Blue" },
335+ {"const" : "yellow" , "title" : "Yellow" },
336+ ]
337+ },
338+ )
339+
340+ @mcp .tool (description = "Single color selection" )
341+ async def select_favorite_color (ctx : Context [ServerSession , None ]) -> str :
342+ result = await ctx .elicit (message = "Select your favorite color" , schema = FavoriteColorSchema )
343+ if result .action == "accept" and result .data :
344+ return f"User: { result .data .user_name } , Favorite: { result .data .favorite_color } "
345+ return f"User { result .action } " # pragma: no cover
346+
347+ # Test multi-select with titles using anyOf
348+ class FavoriteColorsSchema (BaseModel ):
349+ user_name : str = Field (description = "Your name" )
350+ favorite_colors : list [str ] = Field (
351+ description = "Select your favorite colors" ,
352+ json_schema_extra = {
353+ "items" : {
354+ "anyOf" : [
355+ {"const" : "red" , "title" : "Red" },
356+ {"const" : "green" , "title" : "Green" },
357+ {"const" : "blue" , "title" : "Blue" },
358+ {"const" : "yellow" , "title" : "Yellow" },
359+ ]
360+ }
361+ },
362+ )
363+
364+ @mcp .tool (description = "Multiple color selection" )
365+ async def select_favorite_colors (ctx : Context [ServerSession , None ]) -> str :
366+ result = await ctx .elicit (message = "Select your favorite colors" , schema = FavoriteColorsSchema )
367+ if result .action == "accept" and result .data :
368+ return f"User: { result .data .user_name } , Colors: { ', ' .join (result .data .favorite_colors )} "
369+ return f"User { result .action } " # pragma: no cover
370+
371+ # Test legacy enumNames format
372+ class LegacyColorSchema (BaseModel ):
373+ user_name : str = Field (description = "Your name" )
374+ color : str = Field (
375+ description = "Select a color" ,
376+ json_schema_extra = {"enum" : ["red" , "green" , "blue" ], "enumNames" : ["Red" , "Green" , "Blue" ]},
377+ )
378+
379+ @mcp .tool (description = "Legacy enum format" )
380+ async def select_color_legacy (ctx : Context [ServerSession , None ]) -> str :
381+ result = await ctx .elicit (message = "Select a color (legacy format)" , schema = LegacyColorSchema )
382+ if result .action == "accept" and result .data :
383+ return f"User: { result .data .user_name } , Color: { result .data .color } "
384+ return f"User { result .action } " # pragma: no cover
385+
386+ async def enum_callback (context : RequestContext [ClientSession , Any ], params : ElicitRequestParams ):
387+ if "colors" in params .message and "legacy" not in params .message :
388+ return ElicitResult (action = "accept" , content = {"user_name" : "Bob" , "favorite_colors" : ["red" , "green" ]})
389+ elif "color" in params .message :
390+ if "legacy" in params .message :
391+ return ElicitResult (action = "accept" , content = {"user_name" : "Charlie" , "color" : "green" })
392+ else :
393+ return ElicitResult (action = "accept" , content = {"user_name" : "Alice" , "favorite_color" : "blue" })
394+ return ElicitResult (action = "decline" ) # pragma: no cover
395+
396+ # Test single-select with titles
397+ await call_tool_and_assert (mcp , enum_callback , "select_favorite_color" , {}, "User: Alice, Favorite: blue" )
398+
399+ # Test multi-select with titles
400+ await call_tool_and_assert (mcp , enum_callback , "select_favorite_colors" , {}, "User: Bob, Colors: red, green" )
401+
402+ # Test legacy enumNames format
403+ await call_tool_and_assert (mcp , enum_callback , "select_color_legacy" , {}, "User: Charlie, Color: green" )
0 commit comments