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

  1. Wrap non-dict values yourself, e.g. `structured_content={"result": value}`.
  2. Declare an `output_schema` on the tool so automatic serialization produces a valid dict.
  3. Pass the raw value as `content` instead of `structured_content` if structured output isn't needed.
  4. 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

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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/c608a9b0a3aaeed5. Report an issue: GitHub.