Skip to content

Treat typing.Dict[str, X] return annotations like dict[str, X] - #3756

Merged
Kludex merged 4 commits into
mainfrom
structured-output-typing-dict
Oct 24, 2026
Merged

Kludex merged 4 commits into
mainfrom
structured-output-typing-dict

Conversation

@Kludex

@Kludex Kludex commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Fixes #3745

-> typing.Dict[str, X] now gets the same output schema and structuredContent as -> dict[str, X], instead of being wrapped in {"result": ...}.

Motivation and Context

_create_output_model detected generics with isinstance(type_expr, types.GenericAlias). typing.Dict[str, X] is a typing._GenericAlias, so it fell through to the catch-all branch and got wrapped. Rewriting Dict to dict (e.g. ruff UP006) therefore silently changed a tool's output on the wire.

The branch now checks get_origin(type_expr) is not None.

How Has This Been Tested?

  • Added a typing.Dict[str, str] case to test_structured_output_dict_str_types; it fails without the fix.
  • Added a NewType case to test_structured_output_generic_types, which is now what reaches the catch-all branch.
  • func_metadata.py at 100% coverage, strict-no-cover, ruff and pyright pass; test file passes on 3.11, 3.13 and 3.14.

Breaking Changes

Tools annotated -> typing.Dict[str, X] stop wrapping their result in {"result": ...}, matching the builtin spelling. Union, Optional, Literal and typing.List/Tuple now take the generic branch instead of the catch-all, but are still wrapped as before.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I am assigned to the linked issue (or it is labeled help wanted, or I'm a maintainer)
  • I have disclosed any AI assistance and can explain the change in my own words
  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Related: #3725 (bare dict/list/tuple), which touches the same function.

AI Disclaimer

This PR was developed with the assistance of either Claude or Codex. I've reviewed and verified the changes.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T16:49:08.767949Z 9d07811 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Turn on auto-fix | Re-trigger cubic

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d07811d75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +502 to 505
elif (origin := get_origin(type_expr)) is not None:
if origin is dict:
args = get_args(type_expr)
if len(args) == 2 and args[0] is str:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the changed structured-output shape for typing.Dict

For existing tools annotated with typing.Dict[str, X], this changes both the advertised output schema and returned structured content from {"result": {...}} to the dictionary itself. However, no documentation accompanies this user-visible change. Update the existing docs/servers/structured-output.md page to explain that the legacy spelling now behaves like dict[str, X], including the changed wrapping behavior for existing consumers. The repository explicitly requires relevant documentation updates in the same change.

AGENTS.md reference: AGENTS.md:L144-L148

Useful? React with 👍 / 👎.

Comment on lines +302 to +306
assert func_metadata(func_typing_dict).output_schema == {
"type": "object",
"additionalProperties": {"type": "string"},
"title": "func_typing_dictDictOutput",
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use generated snapshots for the new schema assertions

Both new schema assertions compare SDK-produced transformations against handwritten dictionary literals. The test-quality guide's Assertions section requires full-object snapshot(...) equality for transformations and requires generating those snapshots with --inline-snapshot=fix, rather than hand-editing expected literals. Convert the new typing.Dict and NewType expectations to generated snapshots instead of copying the legacy assertion pattern; AGENTS.md explicitly makes that guide applicable when reviewing tests.

AGENTS.md reference: AGENTS.md:L68-L69

Useful? React with 👍 / 👎.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

One optional note from this repository's REVIEW.md or CLAUDE.md checks was not posted as a comment, over this review's limit for such notes; it is on this commit's check card.

Comment thread src/mcp/server/mcpserver/utilities/func_metadata.py
Comment thread tests/server/mcpserver/test_func_metadata.py Outdated
origin = get_origin(type_expr)

# Handle parameterized generics (list[str], typing.Dict[str, int], Union[str, int], etc.)
elif (origin := get_origin(type_expr)) is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Tools returning Annotated[typing.Dict[str, X], Field(...)] lose their pydantic metadata after merging: constraints stop being enforced and descriptions leave output_schema. The dict branch now reached at func_metadata.py:502 builds the model from the stripped type_expr at :508, not from original_annotation. Fix: build the str-keyed dict model from the original annotation, e.g. Annotated[original_annotation, Field(title=...)], so metadata survives for both typing.Dict and builtin dict. The loss is pre-existing for dict[str, X] (see the TODO at :506); this change widens it to the typing.Dict spelling.

Why this was flagged

A tool annotated -> Annotated[typing.Dict[str, int], Field(min_length=1, description="scores")] reaches _create_output_model from func_metadata.py:443 with type_expr = typing.Dict[str, int] and original_annotation still carrying the Field. get_origin is dict, so :502-508 now returns Annotated[type_expr, Field(title=f"{func_name}DictOutput")], which discards the user's Field. On the base, typing.Dict[...] failed isinstance(type_expr, GenericAlias) and fell to the catch-all _create_wrapped_model(func_name, original_annotation), so the schema's result property carried minProperties/description and convert_result (:218) rejected an empty dict. After merging, the schema has neither and an empty dict validates. No safeguard applies: tests/server/mcpserver/test_func_metadata.py:1471 (test_preserves_pydantic_metadata) only covers Annotated[int, Field(gt=1)].

Verification: Triggers when a tool's return annotation is Annotated[typing.Dict[str, X], Field(...)]. In src/mcp/server/mcpserver/utilities/func_metadata.py, on the base typing.Dict[str, int] reached the final else: _create_wrapped_model(func_name, original_annotation). On the head, line 502 now matches and line 508 is built from the stripped type_expr, so the user's Field is gone.

Finding ID: f-f90d2307

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the updated PR and found no bugs; the follow-up commit adds the docs note and the structured_content assertions and drops the # pragma: no cover markers. A maintainer look is still worthwhile because this deliberately changes the wire shape for existing -> typing.Dict[str, X] tools on the stable line.

What was reviewed:

  • Traced _create_output_model in src/mcp/server/mcpserver/utilities/func_metadata.py for annotations that newly enter the get_origin(...) is not None branch: typing.List/Tuple, Union/Optional/X | Y, Literal, typing.Mapping, subscripted user generics and typing.Dict with non-str keys all still end up wrapped, as before.
  • Bare typing.Dict now has origin dict but get_args returns (), so it fails the len(args) == 2 check and stays wrapped; NewType and unsubscripted type aliases have no origin and still reach the catch-all.
  • No other use of the removed GenericAlias import remains in the module.
  • The second commit touches only tests and docs, so I could not confirm from the code whether the other reviewer's inline comment on func_metadata.py was acted on.
Extended reasoning...

The change is three files (+33/-8): one branch condition in _create_output_model, two test cases and a one-line docs parenthetical, touching no auth, injection or data-exposure surface. By reading the code, the only observable behaviour change is that typing.Dict[str, X] returns are no longer wrapped in {"result": ...}; every other annotation that newly takes the generic branch is wrapped exactly as before. That is still a change to published output_schema and structured_content for existing servers, which AGENTS.md treats as an explicit maintainer decision on the 2.x line. An inline comment from another automated reviewer on func_metadata.py predates a follow-up commit that did not touch that file, so it may still be open. No CODEOWNERS file exists in the checkout, and nothing here was executed — the conclusions come from reading the code only.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3756.mcp-python-docs.pages.dev
Deployment https://05466351.mcp-python-docs.pages.dev
Commit c38a652
Triggered by @Kludex
Updated 2026-10-24 17:47:14 UTC

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

@Kludex
Kludex merged commit 5a3a23a into main Oct 24, 2026
39 checks passed
@Kludex
Kludex deleted the structured-output-typing-dict branch October 24, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

typing.Dict[str, X] return annotation is wrapped in {"result": ...}, unlike dict[str, X]

1 participant