-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
FastMCP GitHub Issue: Parameter Type Coercion Ignores str Type Annotations
Title
Parameter Type Coercion Ignores str Type Annotations, Causing Data Loss for UUIDs
Description
FastMCP is coercing string parameters that start with digits to int or float, ignoring explicit str type annotations. This causes permanent data loss for UUIDs and other string identifiers that begin with numeric characters.
Impact
- Data Loss: Original UUID values cannot be recovered once coerced
- Functionality Broken: Tools cannot process UUIDs starting with digits
- Silent Corruption: Coercion happens before function code runs, making it hard to detect
Reproduction
Minimal Example
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP("TestServer")
@mcp.tool()
async def update_task(
ctx: Context,
task_id: str | None = None # Explicitly typed as str
) -> str:
"""Update a task by UUID."""
print(f"Received task_id: {task_id!r}, type: {type(task_id).__name__}")
return f"Task ID: {task_id}"
if __name__ == "__main__":
mcp.run(transport="streamable-http")Test Cases
Case 1: UUID Starting with Digits
- Input:
task_id = "58aa9efd-faad-4901-89e8-99e807a1a2d6" - Expected: Function receives
task_idasstrwith full UUID - Actual: Function receives
task_idasstrwith value"58"(truncated to first digits)
Case 2: UUID with Scientific Notation Pattern
- Input:
task_id = "3400e37e-b251-49d9-91b0-f8dd8602ff7e" - Expected: Function receives
task_idasstrwith full UUID - Actual: Function receives
task_idasstrwith value"3.4e+40"(scientific notation)
Actual Behavior
When calling the tool with a UUID starting with digits:
# Tool call
await update_task(task_id="58aa9efd-faad-4901-89e8-99e807a1a2d6")
# Function receives:
# task_id = "58" (type: str, but value is corrupted)
# Original UUID is permanently lostEvidence from Logs
[UUID DEBUG] manage_task CALLED: action=update, task_id=3.4e+40, task_id_type=str
[UUID DEBUG] manage_task ENTRY: task_id type=str, value=3.4e+40, isinstance(str)=True
[UUID DEBUG] Validation failed: Invalid UUID format for task_id: 3.4e+40. Received as type: str
Key observations:
- Parameter arrives as
strtype (so type annotation is "respected" at the type level) - But the value is corrupted (
"3.4e+40"instead of full UUID) - Original UUID is permanently lost
Root Cause Analysis
The Coercion Pipeline
- MCP Client sends UUID in JSON (as string or number)
- FastMCP JSON Parser processes the request
- If JSON has number: parses as Python
int/float - If JSON has string starting with digits: coerces to number
- If JSON has number: parses as Python
- FastMCP Parameter Processing
- Converts coerced number back to string representation
58→"58"(original UUID58aa9efd-...is lost)3.4e+40→"3.4e+40"(original UUID3400e37e-...is lost)
- Function receives corrupted string
- Type is
str(so type annotation appears respected) - But value is corrupted (original data is lost)
- Type is
Why This Happens
FastMCP appears to:
- Parse JSON values and infer types based on content (not annotations)
- Coerce string values that look like numbers to numeric types
- Convert numeric types back to strings to match type annotations
- But the original string value is lost during coercion
Code Location
The coercion likely happens in:
mcp.server.fastmcp.utilities.func_metadata.FuncMetadata.pre_parse_json()- Or in FastMCP's parameter validation/coercion logic before
call_fn_with_arg_validation()
Expected Behavior
When a parameter is annotated as str | None:
- FastMCP should preserve the string value as-is, regardless of whether it starts with digits
- No type coercion should occur based on value content
- Type annotations should be authoritative for parameter types
Correct Behavior Example
@mcp.tool()
async def update_task(ctx: Context, task_id: str | None = None) -> str:
# If called with: task_id="58aa9efd-faad-4901-89e8-99e807a1a2d6"
# Should receive: task_id="58aa9efd-faad-4901-89e8-99e807a1a2d6" (full UUID)
# NOT: task_id="58" (truncated)
passImpact Assessment
Severity: High
- Data Loss: Permanent loss of UUID values
- Functionality Broken: Tools cannot process many valid UUIDs
- Silent Failure: Coercion happens before user code, making it hard to detect
- Affects Many Use Cases: Any string identifier starting with digits
Affected UUIDs
Any UUID starting with digits will be corrupted:
58aa9efd-...→"58"3400e37e-...→"3.4e+40"12345678-...→"12345678"(might work if all digits, but still wrong)0a1b2c3d-...→"0"(starts with zero)
Real-World Impact
In our production system:
- ~15% of UUIDs start with digits (based on UUID v4 distribution)
- Cannot update/delete tasks with these UUIDs via MCP tools
- Workaround required: Use direct API calls instead of MCP tools
Suggested Fix
Option 1: Respect Type Annotations (Recommended)
FastMCP should use type annotations as the authoritative source for parameter types:
# In func_metadata.py or parameter processing
def process_parameter(value: Any, annotation: type) -> Any:
# If annotation is str (or str | None), preserve as string
if annotation is str or (hasattr(annotation, '__origin__') and str in getattr(annotation, '__args__', [])):
return str(value) if value is not None else None
# Otherwise, use existing type inference
return infer_type(value)Option 2: Disable Content-Based Coercion for Annotated Parameters
Only coerce types when type annotation is ambiguous (e.g., Any):
# Only coerce if annotation doesn't specify a type
if annotation is Any or annotation is None:
# Use content-based type inference
value = coerce_based_on_content(value)
else:
# Respect the annotation
value = coerce_to_annotation(value, annotation)Option 3: Add Explicit Schema Support
Allow tools to specify explicit JSON schemas that override type inference:
@mcp.tool(
input_schema={
"type": "object",
"properties": {
"task_id": {"type": "string"} # Explicit schema
}
}
)
async def update_task(ctx: Context, task_id: str | None = None) -> str:
passWorkarounds (Current)
We've implemented patches to detect the issue, but cannot recover lost data:
# Patch pre_parse_json to detect coercion
def patched_pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
for key, value in data.items():
if key.endswith('_id') and isinstance(value, (int, float)):
# Can detect, but original UUID is already lost
logger.error(f"UUID coercion detected: {key}={value}")
return original_pre_parse_json(self, data)Limitation: Once coerced, original UUID cannot be recovered.
Environment
- FastMCP Version:
mcppackage (latest as of 2026-01-15) - Python Version: 3.12
- Transport:
streamable-http - MCP Client: Cursor IDE (but issue likely affects all clients)
Additional Context
Related Issues
This affects any use case where:
- String identifiers start with digits (UUIDs, IDs, codes)
- Type annotations specify
strbut FastMCP infers numeric types - Data integrity is critical (cannot recover lost values)
Test Code
Full test case demonstrating the issue:
from mcp.server.fastmcp import FastMCP, Context
mcp = FastMCP("UUIDTest")
@mcp.tool()
async def test_uuid(
ctx: Context,
uuid: str # Explicit str annotation
) -> dict:
"""Test UUID parameter handling."""
return {
"received": uuid,
"type": type(uuid).__name__,
"length": len(uuid),
"is_valid_uuid": len(uuid) == 36 and uuid.count('-') == 4
}
# Test cases:
# 1. uuid="58aa9efd-faad-4901-89e8-99e807a1a2d6" → Should return full UUID, but returns "58"
# 2. uuid="3400e37e-b251-49d9-91b0-f8dd8602ff7e" → Should return full UUID, but returns "3.4e+40"Request
Please fix FastMCP to:
- Respect
strtype annotations - Don't coerce string parameters to numbers - Preserve string values - Don't truncate or convert UUID strings
- Use type annotations as authoritative - Don't infer types from content when annotation exists
This is a data loss bug that affects production systems. A fix would be greatly appreciated.
Repository: https://github.com/modelcontextprotocol/python-sdk
Related Files:
mcp/server/fastmcp/utilities/func_metadata.py(likely)- Parameter processing/validation logic
Priority: High (data loss issue)