Skip to content

Commit acf4db0

Browse files
authored
Add SEP-1034 default values support for elicitation (#2545)
- Add comprehensive tests for default values in elicitation schemas - Document default values support in elicitation docs - Confirms FastMCP automatically supports defaults via Pydantic Closes #2544
1 parent 7f8a010 commit acf4db0

File tree

2 files changed

+167
-1
lines changed

2 files changed

+167
-1
lines changed

docs/servers/elicitation.mdx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,41 @@ async def create_task(ctx: Context) -> str:
280280
return "Task creation cancelled"
281281
```
282282

283+
### Default Values
284+
285+
You can provide default values for elicitation fields using Pydantic's `Field(default=...)`. Clients will pre-populate form fields with these defaults, making it easier for users to provide input.
286+
287+
Default values are supported for all primitive types:
288+
- Strings: `Field(default="[email protected]")`
289+
- Integers: `Field(default=50)`
290+
- Numbers: `Field(default=3.14)`
291+
- Booleans: `Field(default=False)`
292+
- Enums: `Field(default=EnumValue.A)`
293+
294+
Fields with default values are automatically marked as optional (not included in the `required` list), so users can accept the default or provide their own value.
295+
296+
```python
297+
from pydantic import BaseModel, Field
298+
from enum import Enum
299+
300+
class Priority(Enum):
301+
LOW = "low"
302+
MEDIUM = "medium"
303+
HIGH = "high"
304+
305+
class TaskDetails(BaseModel):
306+
title: str = Field(description="Task title")
307+
description: str = Field(default="", description="Task description")
308+
priority: Priority = Field(default=Priority.MEDIUM, description="Task priority")
309+
310+
@mcp.tool
311+
async def create_task(ctx: Context) -> str:
312+
result = await ctx.elicit("Please provide task details", response_type=TaskDetails)
313+
if result.action == "accept":
314+
return f"Created: {result.data.title}"
315+
return "Task creation cancelled"
316+
```
317+
283318
## Multi-Turn Elicitation
284319

285320
Tools can make multiple elicitation calls to gather information progressively:

tests/client/test_elicitation.py

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66
from mcp.types import ElicitRequestParams
7-
from pydantic import BaseModel
7+
from pydantic import BaseModel, Field
88
from typing_extensions import TypedDict
99

1010
from fastmcp import Context, FastMCP
@@ -729,3 +729,134 @@ class TaskUpdate:
729729
"Completed",
730730
"On Hold",
731731
]
732+
733+
734+
class TestElicitationDefaults:
735+
"""Test suite for default values in elicitation schemas."""
736+
737+
def test_string_default_preserved(self):
738+
"""Test that string defaults are preserved in the schema."""
739+
740+
class Model(BaseModel):
741+
email: str = Field(default="[email protected]")
742+
743+
schema = get_elicitation_schema(Model)
744+
props = schema.get("properties", {})
745+
746+
assert "email" in props
747+
assert "default" in props["email"]
748+
assert props["email"]["default"] == "[email protected]"
749+
assert props["email"]["type"] == "string"
750+
751+
def test_integer_default_preserved(self):
752+
"""Test that integer defaults are preserved in the schema."""
753+
754+
class Model(BaseModel):
755+
count: int = Field(default=50)
756+
757+
schema = get_elicitation_schema(Model)
758+
props = schema.get("properties", {})
759+
760+
assert "count" in props
761+
assert "default" in props["count"]
762+
assert props["count"]["default"] == 50
763+
assert props["count"]["type"] == "integer"
764+
765+
def test_number_default_preserved(self):
766+
"""Test that number defaults are preserved in the schema."""
767+
768+
class Model(BaseModel):
769+
price: float = Field(default=3.14)
770+
771+
schema = get_elicitation_schema(Model)
772+
props = schema.get("properties", {})
773+
774+
assert "price" in props
775+
assert "default" in props["price"]
776+
assert props["price"]["default"] == 3.14
777+
assert props["price"]["type"] == "number"
778+
779+
def test_boolean_default_preserved(self):
780+
"""Test that boolean defaults are preserved in the schema."""
781+
782+
class Model(BaseModel):
783+
enabled: bool = Field(default=False)
784+
785+
schema = get_elicitation_schema(Model)
786+
props = schema.get("properties", {})
787+
788+
assert "enabled" in props
789+
assert "default" in props["enabled"]
790+
assert props["enabled"]["default"] is False
791+
assert props["enabled"]["type"] == "boolean"
792+
793+
def test_enum_default_preserved(self):
794+
"""Test that enum defaults are preserved in the schema."""
795+
796+
class Priority(Enum):
797+
LOW = "low"
798+
MEDIUM = "medium"
799+
HIGH = "high"
800+
801+
class Model(BaseModel):
802+
choice: Priority = Field(default=Priority.MEDIUM)
803+
804+
schema = get_elicitation_schema(Model)
805+
props = schema.get("properties", {})
806+
807+
assert "choice" in props
808+
assert "default" in props["choice"]
809+
assert props["choice"]["default"] == "medium"
810+
assert "enum" in props["choice"]
811+
assert props["choice"]["type"] == "string"
812+
813+
def test_all_defaults_preserved_together(self):
814+
"""Test that all default types are preserved when used together."""
815+
816+
class Priority(Enum):
817+
A = "A"
818+
B = "B"
819+
820+
class Model(BaseModel):
821+
string_field: str = Field(default="[email protected]")
822+
integer_field: int = Field(default=50)
823+
number_field: float = Field(default=3.14)
824+
boolean_field: bool = Field(default=False)
825+
enum_field: Priority = Field(default=Priority.A)
826+
827+
schema = get_elicitation_schema(Model)
828+
props = schema.get("properties", {})
829+
830+
assert props["string_field"]["default"] == "[email protected]"
831+
assert props["integer_field"]["default"] == 50
832+
assert props["number_field"]["default"] == 3.14
833+
assert props["boolean_field"]["default"] is False
834+
assert props["enum_field"]["default"] == "A"
835+
836+
def test_mixed_defaults_and_required(self):
837+
"""Test that fields with defaults are not in required list."""
838+
839+
class Model(BaseModel):
840+
required_field: str = Field(description="Required field")
841+
optional_with_default: int = Field(default=42)
842+
843+
schema = get_elicitation_schema(Model)
844+
props = schema.get("properties", {})
845+
required = schema.get("required", [])
846+
847+
assert "required_field" in required
848+
assert "optional_with_default" not in required
849+
assert props["optional_with_default"]["default"] == 42
850+
851+
def test_compress_schema_preserves_defaults(self):
852+
"""Test that compress_schema() doesn't strip default values."""
853+
854+
class Model(BaseModel):
855+
string_field: str = Field(default="test")
856+
integer_field: int = Field(default=42)
857+
858+
schema = get_elicitation_schema(Model)
859+
props = schema.get("properties", {})
860+
861+
assert "default" in props["string_field"]
862+
assert "default" in props["integer_field"]

0 commit comments

Comments
 (0)