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