Skip to content

fix: preserve explicit nulls in MCP tool arguments - #303

Open
liangzhechen655 wants to merge 3 commits into
Eigenwise:mainfrom
liangzhechen655:fix/mcp-explicit-null-arguments
Open

liangzhechen655 wants to merge 3 commits into
Eigenwise:mainfrom
liangzhechen655:fix/mcp-explicit-null-arguments

Conversation

@liangzhechen655

@liangzhechen655 liangzhechen655 commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

A generated MCP tool serializes inputs with exclude_none=True, losing an explicitly supplied null even when the server declares that parameter nullable. A required nullable input can become an empty request, and an optional null cannot distinguish clearing a value from omission.

Preserve supplied field presence with exclude_unset=True. Determine nullability from the declared parameter type before the converter widens optional fields to accept None. Only genuinely nullable parameters forward null; locally accepted None on a nonnullable optional parameter stays omitted. Each generated tool stores its own nullability information, including referenced types, and both fresh and persistent sessions use the same serialization.

Validation on Windows / Python 3.13:

  • Three original presence regressions failed before the original change. Eight nonnullable-null cases fail on the previous PR head across fresh and persistent sessions.
  • 35 execution cases cover required/optional nulls, omission, defaults, zero/false, anyOf/oneOf, null and unrestricted types, enum/const restrictions, and independent referenced parameter types across two generated tools.
  • Full core suite: 435 passed, 3 credential-gated integration tests skipped, 93% total coverage.
  • Repository-wide Black and Flake8 pass.
  • Tests use actual generated Pydantic models and tool classes; external MCP session/transport boundaries are mocked. No live LLM or external MCP service was used.

@Eigenwise Eigenwise left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Source review of eabd141ae4fec8598f0aff96f83fc9ee0dbc7016: preserving explicit nulls for nullable arguments is useful, but mcp_factory.py:182 also forwards null for nonnullable optional fields. For an optional property advertised as type: string without a default, the existing generated model accepts an explicitly supplied value=None. The new dump sends {"value": null}; the old dump omitted it. A server validating that advertised schema rejects the new request.

Preserve the distinction between genuinely nullable arguments and locally accepted None for a nonnullable field. Add that nonnullable regression while retaining the required-null, explicit optional-null, omission, default, zero and false cases. Check both fresh and persistent session forwarding.

This finding is source-derived. I haven't run this head's tests or measured its per-function coverage.

@Eigenwise Eigenwise left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Rechecked 301f1eb7c947c24678c4ccacf2d6b60cfaf10b43. Plain nonnullable None is now omitted, nullable null and supplied values are retained, and the per-tool referenced-type case passes. Normal CI passed all 35 new cases, 435 tests total with 3 MiniMax skips; formatting and lint passed.

One advertised-nullability gap remains at mcp_factory.py:67-69: an optional value referencing $defs.Value = {"enum": ["brief"]} is converted to Any, so the TypeAdapter check accepts None and forwards {"value": null}. The advertised enum rejects that request; the base omitted the value. Referenced nonnull const and enum inside anyOf have the same gap. An outer type: string with a nullable anyOf also must not forward null.

Preserve null restrictions through references and applicable schema combinations when deciding whether to forward None. Add focused generated-tool argument assertions for these cases in both session paths, keeping the current passing presence cases. These counterexamples are source-derived, not locally executed.

This branch has not been deployed

No deployments
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.

2 participants