DEV Community

Scott Raisbeck
Scott Raisbeck

Posted on

FastMCP + Claude Desktop: When Optional[X] Type Hints Break Validation

Your MCP server works perfectly. Python tests all green. You deploy to staging, connect Claude Desktop, and immediately hit this error:

Input validation error: '[16]' is not valid under any of the given schemas
Enter fullscreen mode Exit fullscreen mode

You try different formats. Arrays, integers, strings. All fail. Same cryptic message every time.

I spent two hours debugging this evening. Turns out there's a mismatch between how FastMCP's Python client handles optional parameters and what Claude Desktop sends over the wire.

The Crime Scene

I was building an MCP server for a RAG system. Tried to create a document with a project_id:

create_document(
    title="Test Document",
    content="Some content",
    project_id=16  # Integer, seems reasonable
)
Enter fullscreen mode Exit fullscreen mode

Error. Tried the array format for tags:

create_code_artifact(
    title="Test Code",
    code="print('hello')",
    tags=["test", "validation"],  # Arrays work everywhere else
    project_id=16
)
Enter fullscreen mode Exit fullscreen mode

Both parameters failed. Same error, same confusion.

The weird part? Linking worked fine:

link_document_to_project(document_id=25, project_id=16)  # Success
Enter fullscreen mode Exit fullscreen mode

So the backend accepted integers. The validation was happening earlier, at the FastMCP protocol layer before my application code even ran.

The False Start

I dug through my input coercion logic, thinking the type handling was broken. It wasn't. Created test cases. Tried different serialization formats. The pattern emerged slowly: every Optional[X] parameter was failing.

Including one I hadn't tested yet:

mark_obsolete(memory_id=176, superseded_by=178)
Enter fullscreen mode Exit fullscreen mode
Input validation error: '178' is not valid under any of the given schemas
Enter fullscreen mode Exit fullscreen mode

Same error pattern across 13 different parameters in my tool definitions.

The Pattern

I checked my tool signatures:

@mcp.tool()
async def create_document(
    title: str,
    content: str,
    project_id: Optional[int] = None,  # Fails from Claude Desktop
    tags: Optional[List[str]] = None   # Also fails
):
    ...
Enter fullscreen mode Exit fullscreen mode

Standard Python type hints. What everyone uses for optional parameters.

FastMCP generates different JSON schemas depending on how you declare optional parameters:

Optional[List[str]] generates:

{
  "anyOf": [
    {"type": "array", "items": {"type": "string"}},
    {"type": "null"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

List[str] = None generates:

{
  "type": "array",
  "items": {"type": "string"}
}
Enter fullscreen mode Exit fullscreen mode

The first format wasn't working with Claude Desktop. The second one was.

The FastMCP Python client? Handled both formats fine. That's why all my tests passed.

What I Changed

Converted every optional parameter from Optional[X] to X = None:

@mcp.tool()
async def create_document(
    title: str,
    content: str,
    project_id: int = None,      # Changed from Optional[int]
    tags: List[str] = None       # Changed from Optional[List[str]]
):
    ...
Enter fullscreen mode Exit fullscreen mode

13 parameters across 3 files:

memory_tools.py (5):

  • importance_threshold
  • project_ids
  • tags (update)
  • importance (update)
  • superseded_by

document_tools.py (4):

  • size_bytes
  • tags
  • project_id (create + update)

code_artifact_tools.py (4):

  • tags (create + list + update)
  • project_id (create + list + update)

Results

Rebuilt the container. Deployed to staging. Tested every parameter that had been failing:

# All of these now work
create_document(project_id=16)
create_memory(project_ids=[16])
create_code_artifact(tags=["test"], project_id=16)
mark_obsolete(superseded_by=180)
Enter fullscreen mode Exit fullscreen mode

Green across the board.

The Takeaway

Type hints matter at the protocol boundary. Optional[X] is semantically identical to X = None in Python, but i think FastMCP treats them differently when generating JSON schemas. Different MCP clients serialize parameters differently. That was my guess at least, well Claude Desktop's guess at it realised it was occurring on particular fields marked as Optional[], i sometimes have to pinch myself when the system I am working with tells me what the issue is with my integration logic, but this is the world we live in!

This is the kind of bug that's invisible in tests unless you're testing against the actual client. Integration tests with the FastMCP Python client pass. The failure only shows up when Claude Desktop connects (it may happen in other Agents but this was the one where it showed up for me - Claude Code couldn't reproduce it).

There might be other ways to fix this—maybe adjusting FastMCP's schema generation, or handling the anyOf schema differently on the client side. I just know changing the type hints worked for my case.

The fix itself? Five minutes once I found the pattern. The debugging? Two hours of confusion and multiple false starts, until your own software tells you what the problem is!

Dog-fooding works. Eventually.

Top comments (0)