Repository: jlowin/fastmcp

Issue:
Elicitation enum not working as expected
### Description

I tried setting up an MCP server based on this example:
https://gofastmcp.com/servers/elicitation#constrained-options

I first used the Literal type, and then switched to a Python enum. I'm accessing it through Copilot in VS Code. When I use the server with the Literal type and ask to set a priority, I can see the response options (low/medium/high). However, when I use the server with a Python enum and ask to set a priority, I'm only asked to provide free text. Shouldn't Python enum also present the same response options?

<img width="1179" height="389" alt="Image" src="https://github.com/user-attachments/assets/327f3f96-8538-4f9c-998c-fb43bcc846fb" />
<img width="1170" height="389" alt="Image" src="https://github.com/user-attachments/assets/3c8bc52d-392a-462a-8445-8125575c2299" />

### Example Code

```Python
from enum import Enum

from fastmcp import FastMCP, Context

mcp = FastMCP("My MCP Server")

class Priority(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

@mcp.tool
async def set_priority(ctx: Context) -> str:
    """Set task priority level."""
    result = await ctx.elicit("What priority level?", response_type=Priority)
    
    if result.action == "accept":
        return f"Priority set to: {result.data.value}"
    return "No priority set"

if __name__ == "__main__":
    mcp.run()
```

### Version Information

```Text
FastMCP version:                                                                          2.11.3
MCP version:                                                                              1.13.0
Python version:                                                                          3.11.13
Platform:
macOS-15.6-arm64-arm-64bit
```


Discussion:
This looks like a question for the VS Code team.

You'd probably want to pull out MCP Inspector and call list tools and look at the schema coming back for the tools and ask the VS Code team why one of them results in a dropdown and the other results in a free-form input box
I just started to suspect it might be a VS Code problem and not a you problem, thanks!
I actually got a `Tool Result: Error` when I ran the tool in MCP Inspector:
```
Error calling tool 'set_priority': [
  {
    "code": "invalid_union",
    "unionErrors": [
      {
        "issues": [
          {
            "code": "invalid_literal",
            "expected": "boolean",
            "path": [
              "params",
              "requestedSchema",
              "properties",
              "value",
              "type"
            ],
            "message": "Invalid literal value, expected \"boolean\""
          }
        ],
        "name": "ZodError"
      },
      {
        "issues": [
          {
            "code": "invalid_literal",
            "expected": "string",
            "path": [
              "params",
              "requestedSchema",
              "properties",
              "value",
              "type"
            ],
            "message": "Invalid literal value, expected \"string\""
          }
        ],
        "name": "ZodError"
      },
      {
        "issues": [
          {
            "expected": "'number' | 'integer'",
            "received": "undefined",
            "code": "invalid_type",
            "path": [
              "params",
              "requestedSchema",
              "properties",
              "value",
              "type"
            ],
            "message": "Required"
          }
        ],
        "name": "ZodError"
      },
      {
        "issues": [
          {
            "code": "invalid_literal",
            "expected": "string",
            "path": [
              "params",
              "requestedSchema",
              "properties",
              "value",
              "type"
            ],
            "message": "Invalid literal value, expected \"string\""
          },
          {
            "code": "invalid_type",
            "expected": "array",
            "received": "undefined",
            "path": [
              "params",
              "requestedSchema",
              "properties",
              "value",
              "enum"
            ],
            "message": "Required"
          }
        ],
        "name": "ZodError"
      }
    ],
    "path": [
      "params",
      "requestedSchema",
      "properties",
      "value"
    ],
    "message": "Invalid input"
  }
]
```
So the free-form is likely just a silent error in VS Code
Can you please share a minimal reproduction that causes the issue along with the tool schema that MCP Inspector is showing
Of course 😊 thanks so much for your help!

main.py:
```
from enum import Enum

from fastmcp import FastMCP, Context

mcp = FastMCP("My MCP Server")

class Priority(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

@mcp.tool
async def set_priority(ctx: Context) -> str:
    """Set task priority level."""
    result = await ctx.elicit("What priority level?", response_type=Priority)
    
    if result.action == "accept":
        return f"Priority set to: {result.data.value}"
    return "No priority set"

if __name__ == "__main__":
    mcp.run()
```

MCP Inspector:
```
initialize

Request:
{
method:
"initialize"
}

Response:
{
  "capabilities": {
    "experimental": {},
    "prompts": {
      "listChanged": false
    },
    "resources": {
      "subscribe": false,
      "listChanged": false
    },
    "tools": {
      "listChanged": true
    }
  },
  "serverInfo": {
    "name": "My MCP Server",
    "version": "1.13.0"
  }
}

tools/list

Request:
{
  "method": "tools/list",
  "params": {}
}

Response:
{
  "tools": [
    {
      "name": "set_priority",
      "description": "Set task priority level.",
      "inputSchema": {
        "type": "object",
        "properties": {}
      },
      "outputSchema": {
        "type": "object",
        "properties": {
          "result": {
            "title": "Result",
            "type": "string"
          }
        },
        "required": [
          "result"
        ],
        "title": "_WrappedResult",
        "x-fastmcp-wrap-result": true
      },
      "_meta": {
        "_fastmcp": {
          "tags": []
        }
      }
    }
  ]
}

tools/call

Request:
{
  "method": "tools/call",
  "params": {
    "name": "set_priority",
    "arguments": {},
    "_meta": {
      "progressToken": 0
    }
  }
}

Response:
{
  "content": [
    {
      "type": "text",
      "text": "Error calling tool 'set_priority': [\n  {\n    \"code\": \"invalid_union\",\n    \"unionErrors\": [\n      {\n        \"issues\": [\n          {\n            \"code\": \"invalid_literal\",\n            \"expected\": \"boolean\",\n            \"path\": [\n              \"params\",\n              \"requestedSchema\",\n              \"properties\",\n              \"value\",\n              \"type\"\n            ],\n            \"message\": \"Invalid literal value, expected \\\"boolean\\\"\"\n          }\n        ],\n        \"name\": \"ZodError\"\n      },\n      {\n        \"issues\": [\n          {\n            \"code\": \"invalid_literal\",\n            \"expected\": \"string\",\n            \"path\": [\n              \"params\",\n              \"requestedSchema\",\n              \"properties\",\n              \"value\",\n              \"type\"\n            ],\n            \"message\": \"Invalid literal value, expected \\\"string\\\"\"\n          }\n        ],\n        \"name\": \"ZodError\"\n      },\n      {\n        \"issues\": [\n          {\n            \"expected\": \"'number' | 'integer'\",\n            \"received\": \"undefined\",\n            \"code\": \"invalid_type\",\n            \"path\": [\n              \"params\",\n              \"requestedSchema\",\n              \"properties\",\n              \"value\",\n              \"type\"\n            ],\n            \"message\": \"Required\"\n          }\n        ],\n        \"name\": \"ZodError\"\n      },\n      {\n        \"issues\": [\n          {\n            \"code\": \"invalid_literal\",\n            \"expected\": \"string\",\n            \"path\": [\n              \"params\",\n              \"requestedSchema\",\n              \"properties\",\n              \"value\",\n              \"type\"\n            ],\n            \"message\": \"Invalid literal value, expected \\\"string\\\"\"\n          },\n          {\n            \"code\": \"invalid_type\",\n            \"expected\": \"array\",\n            \"received\": \"undefined\",\n            \"path\": [\n              \"params\",\n              \"requestedSchema\",\n              \"properties\",\n              \"value\",\n              \"enum\"\n            ],\n            \"message\": \"Required\"\n          }\n        ],\n        \"name\": \"ZodError\"\n      }\n    ],\n    \"path\": [\n      \"params\",\n      \"requestedSchema\",\n      \"properties\",\n      \"value\"\n    ],\n    \"message\": \"Invalid input\"\n  }\n]"
    }
  ],
  "isError": true
}
```

here's the schema for the elicitation request:
```
{
  "$defs": {
    "Priority": {
      "enum": [
        "low",
        "medium",
        "high"
      ],
      "title": "Priority",
      "type": "string"
    }
  },
  "properties": {
    "value": {
      "$ref": "#/$defs/Priority"
    }
  },
  "required": [
    "value"
  ],
  "title": "ScalarElicitationType",
  "type": "object"
}
```

I wonder if mcp inspector doesn't like that we're using a ref/def here
Oh interesting call. enum's are supported but it looks like not with ref/def syntax, so I think thats a generation bug, see the supported elicitation schemas here: https://modelcontextprotocol.io/specification/draft/client/elicitation#supported-schema-types

```
Supported Schema Types
The schema is restricted to these primitive types:
String Schema

Copy
{
  "type": "string",
  "title": "Display Name",
  "description": "Description text",
  "minLength": 3,
  "maxLength": 50,
  "pattern": "^[A-Za-z]+$",
  "format": "email"
}
Supported formats: email, uri, date, date-time
Number Schema

Copy
{
  "type": "number", // or "integer"
  "title": "Display Name",
  "description": "Description text",
  "minimum": 0,
  "maximum": 100
}
Boolean Schema

Copy
{
  "type": "boolean",
  "title": "Display Name",
  "description": "Description text",
  "default": false
}
Enum Schema

Copy
{
  "type": "string",
  "title": "Display Name",
  "description": "Description text",
  "enum": ["option1", "option2", "option3"],
  "enumNames": ["Option 1", "Option 2", "Option 3"]
}
```

And more specifically https://modelcontextprotocol.io/specification/2025-06-18/schema#enumschema

```
interface ElicitRequest {
  [method](https://modelcontextprotocol.io/specification/2025-06-18/schema#): “elicitation/create”;
  [params](https://modelcontextprotocol.io/specification/2025-06-18/schema#elicitrequest-params): {
    message: string;
    requestedSchema: {
      properties: { [key: string]: [PrimitiveSchemaDefinition](https://modelcontextprotocol.io/specification/2025-06-18/schema#primitiveschemadefinition) };
      required?: string[];
      type: “object”;
    };
  };
}

PrimitiveSchemaDefinition
PrimitiveSchemaDefinition:
  | [StringSchema](https://modelcontextprotocol.io/specification/2025-06-18/schema#stringschema)
  | [NumberSchema](https://modelcontextprotocol.io/specification/2025-06-18/schema#numberschema)
  | [BooleanSchema](https://modelcontextprotocol.io/specification/2025-06-18/schema#booleanschema)
  | [EnumSchema](https://modelcontextprotocol.io/specification/2025-06-18/schema#enumschema)


EnumSchema
interface EnumSchema {
  [description](https://modelcontextprotocol.io/specification/2025-06-18/schema#)?: string;
  [enum](https://modelcontextprotocol.io/specification/2025-06-18/schema#): string[];
  [enumNames](https://modelcontextprotocol.io/specification/2025-06-18/schema#)?: string[];
  [title](https://modelcontextprotocol.io/specification/2025-06-18/schema#)?: string;
  [type](https://modelcontextprotocol.io/specification/2025-06-18/schema#): “string”;
}
[​](https://modelcontextprotocol.io/specification/2025-06-18/schema#imagecontent)
```

I wonder if this is straightofrward enough for /marvin to investigate how we convert enums for use with elicitation requests in the server, and ensure it complies with the spec above?
**Claude finished @jlowin's task** —— [View job](https://github.com/jlowin/fastmcp/actions/runs/17210983584) • [`claude/issue-1565-20250825-1356`](https://github.com/jlowin/fastmcp/tree/claude/issue-1565-20250825-1356) • [Create PR ➔](https://github.com/jlowin/fastmcp/compare/main...claude/issue-1565-20250825-1356?quick_pull=1&title=Fix%20enum%20schema%20generation%20for%20elicitation%20requests&body=Fixes%20%23%23issue%201565%0A%0AThis%20PR%20resolves%20the%20issue%20where%20Python%20enums%20in%20elicitation%20requests%20were%20generating%20JSON%20Schema%20%60%24ref%2F%24defs%60%20syntax%20instead%20of%20inline%20enum%20schemas%20as%20required%20by%20the%20MCP%20specification.%0A%0A%23%23%20Changes%0A-%20Added%20%60_resolve_refs_inline%28%29%60%20function%20to%20resolve%20%60%24ref%60%20references%20inline%0A-%20Modified%20%60get_elicitation_schema%28%29%60%20to%20resolve%20references%20before%20compression%0A-%20Added%20regression%20test%20to%20ensure%20enum%20schemas%20remain%20inlined%0A%0A%23%23%20Impact%0APython%20enums%20in%20elicitation%20requests%20will%20now%20display%20as%20dropdown%20options%20in%20VS%20Code%20instead%20of%20free-text%20input%20fields.%0A%0AGenerated%20with%20%5BClaude%20Code%5D%28https%3A//claude.ai/code%29)

---
### Investigation: Enum Schema Generation for Elicitation Requests ✅

I investigated and fixed the issue where Python enums generate `$ref/$defs` syntax instead of inline enum schemas for MCP elicitation requests.

**Root Cause**: The MCP specification requires inline enum schemas (`{"type": "string", "enum": [...]}`) but FastMCP was generating JSON Schema references (`{"$ref": "#/$defs/Priority"}`).

### Todo List
- [x] Set up development environment 
- [x] Search for elicitation schema generation code
- [x] Identify enum handling logic
- [x] Fix enum schema generation to be inline
- [x] Test the fix with provided example
- [x] Run validation commands
- [x] Commit and push changes

**Solution implemented**: Added `_resolve_refs_inline()` function in `src/fastmcp/server/elicitation.py` that resolves `$ref` references and inlines enum schemas before compression. The schema now correctly generates:

```json
{
  "properties": {"value": {"type": "string", "enum": ["low", "medium", "high"]}}
}
```

**Validation**: 
- All 2594 existing tests pass
- Added new regression test `test_enum_schema_inlined_correctly`
- Pre-commit hooks passed (ruff, prettier, ty)
- Tested with the exact example from the issue

The fix ensures that Python enums will now display as dropdown options in VS Code instead of free-text input fields.



---



Patch:
diff --git a/src/fastmcp/server/elicitation.py b/src/fastmcp/server/elicitation.py
index 8066aa727..a7216a6b0 100644
--- a/src/fastmcp/server/elicitation.py
+++ b/src/fastmcp/server/elicitation.py
@@ -8,6 +8,8 @@
     DeclinedElicitation,
 )
 from pydantic import BaseModel
+from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
+from pydantic_core import core_schema
 
 from fastmcp.utilities.json_schema import compress_schema
 from fastmcp.utilities.logging import get_logger
@@ -26,6 +28,60 @@
 T = TypeVar("T")
 
 
+class ElicitationJsonSchema(GenerateJsonSchema):
+    """Custom JSON schema generator for MCP elicitation that always inlines enums.
+
+    MCP elicitation requires inline enum schemas without $ref/$defs references.
+    This generator ensures enums are always generated inline for compatibility.
+    Optionally adds enumNames for better UI display when available.
+    """
+
+    def generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue:
+        """Override to prevent ref generation for enums."""
+        # For enum schemas, bypass the ref mechanism entirely
+        if schema["type"] == "enum":
+            # Directly call our custom enum_schema without going through handler
+            # This prevents the ref/defs mechanism from being invoked
+            return self.enum_schema(schema)
+        # For all other types, use the default implementation
+        return super().generate_inner(schema)
+
+    def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
+        """Generate inline enum schema with optional enumNames for better UI.
+
+        If enum members have a _display_name_ attribute or custom __str__,
+        we'll include enumNames for better UI representation.
+        """
+        # Get the base schema from parent
+        result = super().enum_schema(schema)
+
+        # Try to add enumNames if the enum has display-friendly names
+        enum_cls = schema.get("cls")
+        if enum_cls:
+            members = schema.get("members", [])
+            enum_names = []
+            has_custom_names = False
+
+            for member in members:
+                # Check if member has a custom display name attribute
+                if hasattr(member, "_display_name_"):
+                    enum_names.append(member._display_name_)
+                    has_custom_names = True
+                # Or use the member name with better formatting
+                else:
+                    # Convert SNAKE_CASE to Title Case for display
+                    display_name = member.name.replace("_", " ").title()
+                    enum_names.append(display_name)
+                    if display_name != member.value:
+                        has_custom_names = True
+
+            # Only add enumNames if they differ from the values
+            if has_custom_names:
+                result["enumNames"] = enum_names
+
+        return result
+
+
 # we can't use the low-level AcceptedElicitation because it only works with BaseModels
 class AcceptedElicitation(BaseModel, Generic[T]):
     """Result when user accepts the elicitation."""
@@ -46,7 +102,10 @@ def get_elicitation_schema(response_type: type[T]) -> dict[str, Any]:
         response_type: The type of the response
     """
 
-    schema = get_cached_typeadapter(response_type).json_schema()
+    # Use custom schema generator that inlines enums for MCP compatibility
+    schema = get_cached_typeadapter(response_type).json_schema(
+        schema_generator=ElicitationJsonSchema
+    )
     schema = compress_schema(schema)
 
     # Validate the schema to ensure it follows MCP elicitation requirements


Test patch:
diff --git a/tests/client/test_elicitation.py b/tests/client/test_elicitation.py
index 51b6b23db..fcf7a94ca 100644
--- a/tests/client/test_elicitation.py
+++ b/tests/client/test_elicitation.py
@@ -15,6 +15,7 @@
     AcceptedElicitation,
     CancelledElicitation,
     DeclinedElicitation,
+    get_elicitation_schema,
     validate_elicitation_json_schema,
 )
 from fastmcp.utilities.types import TypeAdapter
@@ -639,3 +640,80 @@ async def elicitation_handler(message, response_type, params, ctx):
             match="Elicitation responses must be serializable as a JSON object",
         ):
             await client.call_tool("ask_for_name")
+
+
+def test_enum_elicitation_schema_inline():
+    """Test that enum schemas are generated inline without $ref/$defs for MCP compatibility."""
+
+    class Priority(Enum):
+        LOW = "low"
+        MEDIUM = "medium"
+        HIGH = "high"
+
+    @dataclass
+    class TaskRequest:
+        title: str
+        priority: Priority
+
+    # Generate elicitation schema
+    schema = get_elicitation_schema(TaskRequest)
+
+    # Verify no $defs section exists (enums should be inlined)
+    assert "$defs" not in schema, (
+        "Schema should not contain $defs - enums must be inline"
+    )
+
+    # Verify no $ref in properties
+    for prop_name, prop_schema in schema.get("properties", {}).items():
+        assert "$ref" not in prop_schema, (
+            f"Property {prop_name} contains $ref - should be inline"
+        )
+
+    # Verify the priority field has inline enum values
+    priority_schema = schema["properties"]["priority"]
+    assert "enum" in priority_schema, "Priority should have enum values inline"
+    assert priority_schema["enum"] == ["low", "medium", "high"]
+    assert priority_schema.get("type") == "string"
+
+    # Verify title field is a simple string
+    assert schema["properties"]["title"]["type"] == "string"
+
+
+def test_enum_elicitation_schema_with_enum_names():
+    """Test that enum schemas can include enumNames for better UI display."""
+
+    class TaskStatus(Enum):
+        NOT_STARTED = "not_started"
+        IN_PROGRESS = "in_progress"
+        COMPLETED = "completed"
+        ON_HOLD = "on_hold"
+
+    @dataclass
+    class TaskUpdate:
+        task_id: str
+        status: TaskStatus
+
+    # Generate elicitation schema
+    schema = get_elicitation_schema(TaskUpdate)
+
+    # Verify enum is inline
+    assert "$defs" not in schema
+    assert "$ref" not in str(schema)
+
+    status_schema = schema["properties"]["status"]
+    assert "enum" in status_schema
+    assert status_schema["enum"] == [
+        "not_started",
+        "in_progress",
+        "completed",
+        "on_hold",
+    ]
+
+    # Check if enumNames were added for display
+    assert "enumNames" in status_schema
+    assert status_schema["enumNames"] == [
+        "Not Started",
+        "In Progress",
+        "Completed",
+        "On Hold",
+    ]