Skip to content

NotRequired TypedDict return annotation raises PydanticForbiddenQualifier at tool registration on Python 3.10 #3227

Description

@sainikhiljuluri

Release line

2.x (main), reproduced at a4f4ccd0.

Bug description

On Python 3.10 a tool whose return annotation is a TypedDict containing a NotRequired key
cannot be registered at all. @mcp.tool() raises while the decorator runs, so the failure lands at
import time and the tool never exists.

3.11 and above are unaffected. The project supports 3.10 (requires-python = ">=3.10", and CI runs
a full 3.10 matrix), and TypedDict is one of the documented structured-output return shapes, so on
that interpreter the documented feature is unusable.

Steps to reproduce

import sys
from typing_extensions import NotRequired, TypedDict
from mcp.server.mcpserver import MCPServer

class Person(TypedDict):
    name: str
    age: NotRequired[int]

mcp = MCPServer("td")
print("python", sys.version.split()[0])

@mcp.tool()
def get_person() -> Person:
    return {"name": "Dave"}
uv run --python 3.10 --frozen --all-extras --dev python repro.py

Actual behaviour

python 3.10.19
pydantic.errors.PydanticForbiddenQualifier: The annotation 'NotRequired[int]' contains the
'typing.NotRequired' type qualifier, which is invalid in the context it is defined.

The same file on 3.13 registers the tool without complaint.

Expected behaviour

The tool registers on every supported interpreter, as it already does on 3.11+.

Root cause

_create_model_from_typeddict (src/mcp/server/mcpserver/utilities/func_metadata.py) resolves the
annotations with typing.get_type_hints and passes each one straight to create_model:

type_hints = get_type_hints(td_type)
...
model_fields[field_name] = (field_type, None)

typing.get_type_hints only strips the Required / NotRequired qualifier from 3.11 onwards.
Below that field_type is still NotRequired[int], and pydantic rejects a bare qualifier outside a
TypedDict context.

Observed directly:

python 3.10.19  typing.get_type_hints(Person) -> {'name': str, 'age': typing_extensions.NotRequired[int]}
python 3.13.12  typing.get_type_hints(Person) -> {'name': str, 'age': int}

typing_extensions.get_type_hints returns the stripped form on both.

Why the tests do not catch it

test_structured_output_typeddict uses all-required keys, and the total=False variants express
optionality through __optional_keys__ rather than a qualifier in the annotation, so no existing
test constructs a NotRequired[...] annotation.

Note

A fix is already included in #3225, because the tests added there cannot pass on 3.10 without it.
Filing separately since the defect is independent of that PR's subject and is worth tracking on its
own — happy to split the fix out into its own PR if you would prefer.

Disclosure

Investigated with AI assistance (Claude Code). Filed by me as the human contributor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions