PrefectHQ/fastmcp · error
structured_content must be a dict or None. Got {type(structu
Error message
structured_content must be a dict or None. Got {type(structured_content).__name__}: {structured_content!r}. Tools should wrap non-dict values based on their output_schema. What it means
ToolResult.structured_content must be a dict (matching MCP's object-shaped structured results) or None. Non-dict values (list, str, int, etc.) cannot be sent as structured content directly; tools must wrap such values per their output_schema (which produces a {"result": ...} dict). The constructor raises ValueError naming the offending type and value.
Source
Thrown at fastmcp_slim/fastmcp/tools/base.py:146
if structured_content is not None:
# Convert Prefab types to their wire-format envelope before
# generic serialization, so the renderer gets the right shape.
if is_prefab_app(structured_content):
structured_content = _prefab_to_json(structured_content)
elif is_prefab_component(structured_content):
structured_content = _prefab_to_json(
prefab_app_from_component(structured_content)
)
try:
structured_content = _serialize_to_jsonable(structured_content)
except pydantic_core.PydanticSerializationError as e:
logger.error(
f"Could not serialize structured content. If this is unexpected, set your tool's output_schema to None to disable automatic serialization: {e}"
)
raise
if not isinstance(structured_content, dict):
raise ValueError(
"structured_content must be a dict or None. "
f"Got {type(structured_content).__name__}: {structured_content!r}. "
"Tools should wrap non-dict values based on their output_schema."
)
super().__init__(
content=converted_content,
structured_content=structured_content,
meta=meta,
is_error=is_error,
)
@classmethod
def from_mcp_result(cls, result: CallToolResult) -> ToolResult:
"""Wrap a protocol result while preserving its exact wire representation."""
tool_result = cls(
content=result.content,
structured_content=result.structured_content,View on GitHub (pinned to 1f02114297)
Solutions
- Wrap non-dict values yourself, e.g. `structured_content={"result": value}`.
- Declare an `output_schema` on the tool so automatic serialization produces a valid dict.
- Pass the raw value as `content` instead of `structured_content` if structured output isn't needed.
- If serialization is intentionally disabled, set the tool's output_schema to None and rely on content only.
Example fix
# before
return ToolResult(structured_content=[1, 2, 3])
# after
return ToolResult(content=[1, 2, 3], structured_content={"result": [1, 2, 3]}) Defensive patterns
Strategy: type-guard
Validate before calling
def wrap_structured(value):
if value is None:
return None
if not isinstance(value, dict):
return {"result": value}
return value
result = ToolResult(content=value, structured_content=wrap_structured(value)) Type guard
def is_valid_structured(v) -> bool:
return v is None or isinstance(v, dict) Try / catch
try:
return ToolResult(structured_content=value)
except ValueError as e:
if "must be a dict" in str(e):
return ToolResult(content=value, structured_content={"result": value})
raise Prevention
- Declare an output_schema on tools so non-dict returns are auto-wrapped.
- Never hand-build structured_content from raw tool values without wrapping.
- Add a test asserting every structured result your server emits is a dict.
When it happens
Trigger: Constructing `ToolResult(structured_content=[1,2,3])` or with a scalar/string; returning a bare non-dict from code that bypasses the output_schema wrapping path (e.g. manually building results instead of relying on FunctionTool serialization).
Common situations: Manually constructing ToolResult from raw tool return values without applying output_schema wrapping; tools returning lists or primitives while authors assume auto-wrapping happens in ToolResult itself; migration from older code paths where wrapping was done elsewhere.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- Either content or structured_content must be provided
- Cannot resolve tool reference: {fn!r}
- Cannot determine tool name for {fn!r}
- File {name!r} not found. Available: {available}
- File {f.get('name', '?')!r} exceeds max size ({_format_size(
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/c608a9b0a3aaeed5.
Report an issue: GitHub.