structuredContent for a TypedDict return injects nulls for NotRequired keys, violating the tool's own outputSchema · Issue #3224 · modelcontextprotocol/python-sdk · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
structuredContent for a TypedDict return injects nulls for NotRequired keys, violating the tool's own outputSchema #3224
A tool whose return annotation is a TypedDict with NotRequired keys (or total=False)
publishes an outputSchema in which those properties are non-nullable, but structuredContent materialises every key the tool did not return as an explicit null.
The response therefore violates the schema the same server just advertised, and a conforming
client rejects the call. The SDK's own client does exactly that, so these tools are effectively
unusable — even though TypedDict is one of the return shapes structured output documents.
The unstructured text block and structuredContent also disagree, so a client that skips
validation sees two different answers for the same call.
outputSchema: {"properties": {"age": {"default": null, "title": "Age", "type": "integer"},
"name": {"title": "Name", "type": "string"},
"nickname": {"default": null, "title": "Nickname", "type": "string"}},
"required": ["name"], "title": "Person", "type": "object"}
RuntimeError: Invalid structured content returned by tool get_person: None is not of type 'string'
Failed validating 'type' in schema['properties']['nickname']:
{'default': None, 'title': 'Nickname', 'type': 'string'}
On instance['nickname']:
None
structuredContent is {"name": "Dave", "age": None, "nickname": None} while the text block is {"name": "Dave"}.
Expected behaviour
structuredContent is {"name": "Dave"} — a NotRequired key the tool omitted is absent, not
null — which validates against the published schema, and the call succeeds.
Root cause
_create_model_from_typeddict (src/mcp/server/mcpserver/utilities/func_metadata.py) gives each
non-required key a Nonedefault to make it optional on the generated model; the declared type
stays non-nullable, so the emitted schema is {"type": "integer", "default": null} and the key is
absent from required. convert_result then dumps with model_dump(mode="json", by_alias=True) and no exclude_unset.
The file's own comment already prescribes the fix:
# For optional TypedDict fields, set default=None# This makes them not required in the Pydantic model# The model should use exclude_unset=True when dumping to get TypedDict semantics
Nothing applies it.
Why the tests do not catch it
tests/server/mcpserver/test_func_metadata.py::test_structured_output_typeddict covers exactly
this shape but asserts only the schema, and marks the tool body # pragma: no cover — the tool is
never invoked, so convert_result is never exercised for a partial TypedDict.
That pair concerns the validation-vs-serialization JSON-schema mode. model_json_schema(mode="serialization")
produces byte-identical output for a (int, None) field, so it does not address this. Note #3118 does
edit adjacent lines, so ordering matters if both land.
Disclosure
Investigated with AI assistance (Claude Code). I have a fix and tests ready and will open a PR
linked to this issue.
Activity
Ecocitizenz commentedon Jul 31, 2026