inspector icon indicating copy to clipboard operation
inspector copied to clipboard

Dropdown for enums

Open laurelthorburn opened this issue 4 months ago • 17 comments

Is your feature request related to a problem? Please describe. I'm always sad when using MCP tools with enum parameters because the Inspector renders them as text input fields despite having the valid options available in the Zod schema. Users must manually type values like "some very long value because I can't provide my actual example because I have a mortgage to pay" without seeing the available options, leading to typos and validation errors. The valid options are only visible in the description text, requiring users to read carefully and type precisely.

Describe the solution you'd like When the Inspector detects a z.enum() schema for a tool parameter, it should render a <select> dropdown instead of a text input. The dropdown would populate its options from the enum values already available in the Zod schema. For example, z.enum(["some option one", "some option two", "some option three"]) would render as a dropdown with those three options, with any default value pre-selected (possibly even just first in array).

Describe alternatives you've considered

  1. Autocomplete text input: Add typeahead suggestions based on enum values, but this still requires typing
  2. Radio buttons: For small enums (< say 5 options), but doesn't scale well for larger lists
  3. Better inline documentation: Show valid options more prominently near the input, but doesn't prevent typos

A dropdown remains the most intuitive UI pattern for selecting from predefined options and supports me being as lazy as possible.

Additional context Since the Inspector already uses Zod for validation and has access to the enum values via the schema, implementing this should be straightforward. The type information is already there - it just needs to be used for rendering.

Example schema that should trigger dropdown rendering:

z.enum(["option one", "option two", "option three", "option four", "option five", "option six"])
  .optional()
  .default("option one")

This would benefit all MCP servers using enum parameters, improving user experience and reducing input errors.

Thank you 🤖💕

laurelthorburn avatar Aug 27 '25 03:08 laurelthorburn

The fix doesn't work with FastMCP enum parameters

Hi! I've tested the enum dropdown feature from #842 and unfortunately it doesn't work with FastMCP-generated tools. The issue is that FastMCP uses JSON Schema $ref references instead of inline enums.

Minimal reproducible example

  from fastmcp import FastMCP
  from enum import Enum

  mcp = FastMCP("Demo Enum 🚀")

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

  @mcp.tool
  def set_priority(priority: Priority = Priority.LOW) -> str:
      return f"Priority set to: {priority}"

  if __name__ == "__main__":
      mcp.run(transport="http", port=8005)

The problem

When the Inspector calls tools/list, FastMCP returns this JSON schema:

  {
    "name": "set_priority",
    "inputSchema": {
      "type": "object",
      "properties": {
        "priority": {
          "anyOf": [
            {
              "$ref": "#/$defs/Priority"
            },
            {
              "type": "null"
            }
          ],
          "default": "low"
        }
      },
      "$defs": {
        "Priority": {
          "type": "string",
          "enum": ["low", "medium", "high"]
        }
      }
    }
  }

The current implementation in ToolsTab.tsx only detects inline enums:

prop.type === "string" && prop.enum

But FastMCP places the enum definition in $defs (to reuse type definitions across multiple parameters) and references it via $ref, so the dropdown is never rendered.

Suggested fix

  • Resolve $ref references from $defs or definitions
  • Handle refs inside anyOf/oneOf (FastMCP wraps refs in anyOf with null for optional params)
  • Extract the enum after resolution

I can submit a PR with the fix if helpful - I've already implemented and tested it locally. All tests pass and it works with both inline enums and FastMCP-style $ref enums.

@olaservo: Hope that helps!

juancarlosm avatar Oct 11 '25 07:10 juancarlosm

This would benefit all MCP servers using enum parameters, improving user experience and reducing input errors.

Part of the problem is the EnumSchema in the spec. It's wrong.

There is an SEP to improve it and add multi-select in addition to single-select modes.

In the meantime, we have added support for standard enums (without titles).

cliffhall avatar Oct 11 '25 15:10 cliffhall

I don't quite follow what we should be using for enums with the FastMCP server. Are you saying that FastMCP has implemented enums incorrectly? This is the server we're trying to use: https://github.com/Azure-Samples/python-mcp-demo/blob/main/basic_mcp_http.py#L37

The inspector shows those enums as a JSON field right now, and it's unclear what to enter in them.

Image

pamelafox avatar Oct 23 '25 12:10 pamelafox

Is your feature request related to a problem? Please describe. I'm always sad when using MCP tools with enum parameters because the Inspector renders them as text input fields despite having the valid options available in the Zod schema. Users must manually type values like "some very long value because I can't provide my actual example because I have a mortgage to pay" without seeing the available options, leading to typos and validation errors. The valid options are only visible in the description text, requiring users to read carefully and type precisely.

Describe the solution you'd like When the Inspector detects a z.enum() schema for a tool parameter, it should render a <select> dropdown instead of a text input. The dropdown would populate its options from the enum values already available in the Zod schema. For example, z.enum(["some option one", "some option two", "some option three"]) would render as a dropdown with those three options, with any default value pre-selected (possibly even just first in array).

Describe alternatives you've considered

  1. Autocomplete text input: Add typeahead suggestions based on enum values, but this still requires typing
  2. Radio buttons: For small enums (< say 5 options), but doesn't scale well for larger lists
  3. Better inline documentation: Show valid options more prominently near the input, but doesn't prevent typos

A dropdown remains the most intuitive UI pattern for selecting from predefined options and supports me being as lazy as possible.

Additional context Since the Inspector already uses Zod for validation and has access to the enum values via the schema, implementing this should be straightforward. The type information is already there - it just needs to be used for rendering.

Example schema that should trigger dropdown rendering:

z.enum(["option one", "option two", "option three", "option four", "option five", "option six"]) .optional() .default("option one") This would benefit all MCP servers using enum parameters, improving user experience and reducing input errors.

Thank you 🤖💕

Ty

FMaxey3 avatar Oct 23 '25 15:10 FMaxey3

I don't quite follow what we should be using for enums with the FastMCP server. Are you saying that FastMCP has implemented enums incorrectly? This is the server we're trying to use: https://github.com/Azure-Samples/python-mcp-demo/blob/main/basic_mcp_http.py#L37

The inspector shows those enums as a JSON field right now, and it's unclear what to enter in them.

Hi @pamelafox!

Not sure what version you're on. Can you try the latest version of the inspector?

npx @modelcontextprotocol/inspector@latest

Below is what I see currently, running the gzip tool of the everything reference server. Notice the tool has an enum for the outputType parameter in its inputSchema, and the UI renders it as a dropdown.

If just updating to the latest version of the inspector doesn't fix your problem:

When you run your server, open up the tools/list as I have in this screenshot, and drill down to your category parameter. Please include a screenshot of the whole Inspector surface with both that parameter shown in the tools/list response and the input field visible, as I have here:

Image

cliffhall avatar Oct 23 '25 15:10 cliffhall

I just ran @latest and am still seeing a text field: Image

Here's the response from tools/list:

{
  "tools": [
    {
      "name": "add_expense",
      "description": "Add a new expense to the expenses.csv file.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "date": {
            "description": "Date of the expense in YYYY-MM-DD format",
            "format": "date",
            "type": "string"
          },
          "amount": {
            "description": "Positive numeric amount of the expense",
            "type": "number"
          },
          "category": {
            "$ref": "#/$defs/Category",
            "description": "Category label"
          },
          "description": {
            "description": "Human-readable description of the expense",
            "type": "string"
          },
          "payment_method": {
            "$ref": "#/$defs/PaymentMethod",
            "description": "Payment method used"
          }
        },
        "required": [
          "date",
          "amount",
          "category",
          "description",
          "payment_method"
        ],
        "$defs": {
          "Category": {
            "enum": [
              "food",
              "transport",
              "entertainment",
              "shopping",
              "gadget",
              "other"
            ],
            "type": "string"
          },
          "PaymentMethod": {
            "enum": [
              "amex",
              "visa",
              "cash"
            ],
            "type": "string"
          }
        }
      },
      "_meta": {
        "_fastmcp": {
          "tags": []
        }
      }
    }
  ]
}

pamelafox avatar Oct 23 '25 15:10 pamelafox

Looks like the gzip uses a different way of defining enums? Is the $ref way unsupported? Should I file an issue in fastmcp repo?

pamelafox avatar Oct 23 '25 15:10 pamelafox

I also don't understand how to make it work, I have the same issue as @pamelafox

Image

jjhidalgar-celonis avatar Oct 24 '25 10:10 jjhidalgar-celonis

@pamelafox @jjhidalgar-celonis Yep, it looks like the $refs and $defs is what's throwing us here. We do not have support for them in the current implementation. And, IMHO, it would be a bad thing to support. Not saying we won't, but hear me out...

Aren't $defs & $ref valid for JSON schemas?

Absolutely. In a large schema, like the MCP schema, for instance, using $refs and $defs makes sense. That's because you may define (in $defs) an entity that will then be referred to ($ref) multiple times elsewhere in the schema.

For example in the screenshot below, we're referring to an entity definition called Role. A simple search tells us that this entity definition is referred to four times in the schema. That's a net savings over defining Role in each place.

Image

Then why is it bad in a tool schema?

In the small-world of a tool's inputSchema, where are we going to find that savings? If a definition is only referred to one time, as in both of your cases, it is actually a net surplus of tokens, in addition to being more difficult for the model to reason about.

You're extracting the guts of a property definition out to a separate place ($defs), giving it a name, and then referring to it exactly once. In coding, you only extract lines into a function when you're going to call them more than once. The same guidance should apply to schemas.

Image Image

So what can I do about it if fast-mcp is doing this automagically?

Take care with how you write your schemas. The fast-mcp library has typescript and python implementations. The former uses Zod, the latter Pydandic, and those are the source of the $defs & $ref outputs.

Typescript fast-mcp

The key is to avoid using .refine() or reusing schema definitions, since those trigger the use of $defs and $ref for deduplication. Here are some strategies:

Inline everything directly in the tool definition

import { FastMCP } from 'fast-mcp';
import { z } from 'zod';

const mcp = new FastMCP('my-server');

mcp.tool({
  name: 'example_tool',
  description: 'Example with inline schemas',
  parameters: z.object({
    user: z.object({
      name: z.string(),
      age: z.number().int().positive(),
      email: z.string().email()
    }),
    preferences: z.object({
      theme: z.enum(['light', 'dark']),
      notifications: z.boolean()
    })
  }),
  execute: async ({ user, preferences }) => {
    // implementation
  }
});

This will generate a schema with all properties defined inline, no $defs.

Avoid schema reuse

If you define a schema once and reuse it multiple times, Zod's JSON Schema generator will automatically extract it into $defs:

// This WILL create $defs
const UserSchema = z.object({ name: z.string() });

mcp.tool({
  parameters: z.object({
    user1: UserSchema,  // Referenced twice
    user2: UserSchema   // So it goes to $defs
  })
});

Instead, inline the definition each time if you want to avoid refs:

// This WON'T create $defs
mcp.tool({
  parameters: z.object({
    user1: z.object({ name: z.string() }),
    user2: z.object({ name: z.string() })
  })
});

Python fast-mcp

Inline definitions with Pydantic

The key is to define nested models inline using TypedDict or inline Field() definitions rather than creating separate Pydantic model classes:

from fast_mcp import FastMCP
from pydantic import BaseModel, Field
from typing import Literal

mcp = FastMCP("my-server")

@mcp.tool()
def example_tool(
    user_name: str = Field(description="User's name"),
    user_age: int = Field(gt=0, description="User's age"),
    theme: Literal["light", "dark"] = Field(description="UI theme"),
    notifications: bool = Field(description="Enable notifications")
):
"""Example with inline parameter definitions"""
    pass

Avoid separate Pydantic models for single-use cases

If you define a separate Pydantic model and use it as a parameter type, it will often get extracted to $defs:

# This WILL likely create $defs
class User(BaseModel):
    name: str
    age: int

@mcp.tool()
def process_user(user: User):
    """Process a user"""
    pass

Use nested inline models sparingly

If you need nested objects, you can sometimes use dictionaries with type hints:

from typing import Dict, Any

@mcp.tool()
def process_data(
    config: Dict[str, Any] = Field(
        description="Configuration with theme and notifications"
    )
):
    """Process with dict instead of nested model"""
    pass

However, this loses type validation benefits.

Check Pydantic's schema generation config

Pydantic v2 has a model_config option that can control schema generation. You might be able to set:

class MyModel(BaseModel):
    model_config = {
        "json_schema_mode": "validation",
        "json_schema_serialization_defaults_required": True
    }

Though this may not directly control $defs usage.

The Python version is a bit more opinionated about structure since Pydantic is designed around model reuse. If $defs are still appearing with inline definitions, you might need to check fast-mcp's specific implementation or consider flattening your parameter structure to avoid nested objects entirely.

cliffhall avatar Oct 24 '25 18:10 cliffhall

@cliffhall I'm not sure if FastMCP supports other ways of generating enum outputs, I've filed an issue here: https://github.com/jlowin/fastmcp/issues/2236

pamelafox avatar Oct 24 '25 19:10 pamelafox

I'm not sure if FastMCP supports other ways of generating enum outputs, I've filed an issue here: jlowin/fastmcp#2236

@juancarlosm @pamelafox please have another look at my comment above. I was not finished with it and accidentally submitted before finishing the final section on how to write your schemas.

cliffhall avatar Oct 24 '25 19:10 cliffhall

@cliffhall Thanks, thats helpful! I do generally recommend that Python developers use Enum in these situations, because then they can keep using that Enum later in the code, like in if/else statements - that avoids ever relying on string literals. So I think I'll keep my issue in the fastmcp repo, as maybe it'd make sense for them to always avoid refs/defs in their outputs?

pamelafox avatar Oct 24 '25 19:10 pamelafox

I do generally recommend that Python developers use Enum in these situations, because then they can keep using that Enum later in the code, like in if/else statements - that avoids ever relying on string literals.

@pamelafox This is a place where practicality runs up against coding best practices. I agree defining enums for use throughout the code is a good thing. And you could define an enum for use everywhere else in the code but just use Literal in the inputSchema definition. This would suppress the $def $ref that makes the schema larger and more complex than it needs to be and more difficult for a model to reason about, while still allowing you to refer to it elsewhere in your code. It's a tradeoff.

I think I'll keep my issue in the fastmcp repo, as maybe it'd make sense for them to always avoid refs/defs in their outputs?

I commented on your issue. If they can do something to mitigate that would be great, but ultimately its Zod or Pydantic calling the shots on this part of the output I believe.

cliffhall avatar Oct 24 '25 19:10 cliffhall

@olaservo @evalstate I'd appreciate any input you might have on whether the Inspector should support $defs & $refs in tool inputSchemas . My hot take is above.

cliffhall avatar Oct 24 '25 20:10 cliffhall

@cliffhall thank you for your detailed explanation! I really agree with you, the LLM will also benefit as it's easier to understand direct fields than using defs and refs.

jjhidalgar-celonis avatar Oct 25 '25 11:10 jjhidalgar-celonis

Hi @cliffhall @evalstate @KKonstantinov I think we have another example of adding support for $refs here, in this case for elicitations: https://github.com/modelcontextprotocol/inspector/pull/902 and this one? https://github.com/modelcontextprotocol/inspector/pull/901

I agree it seems like its overcomplicating these schema definitions to structure them with defs and refs. It seems like we're debating whether we should support anything that is technically valid, vs things we think are a best practice for structuring inputs?

olaservo avatar Nov 04 '25 04:11 olaservo

It seems like we're debating whether we should support anything that is technically valid, vs things we think are a best practice for structuring inputs?

@olaservo Yes, that was my hot take. But after absolutely leaning into, I think there's no getting around it. The creation of the actual schema that's sent to the client is usually a bit removed from the process of defining the inputs and fast-mcp is absolutely leaning into outputting $defs/refs. So we might as well support changes to handle $refs and look into adding support for $defs.

To that end https://github.com/modelcontextprotocol/inspector/pull/889 was just merged last week which handles $refs in /properties.

cliffhall avatar Nov 04 '25 14:11 cliffhall