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
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
)
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
)
Both parameters failed. Same error, same confusion.
The weird part? Linking worked fine:
link_document_to_project(document_id=25, project_id=16) # Success
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)
Input validation error: '178' is not valid under any of the given schemas
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
):
...
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"}
]
}
List[str] = None
generates:
{
"type": "array",
"items": {"type": "string"}
}
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]]
):
...
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)
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)