Skip to content

MicrosoftExtensions: Normalize nullable union tool types#72

Open
PederHP wants to merge 1 commit intoanthropics:nextfrom
PederHP:meai_fix_tool_types
Open

MicrosoftExtensions: Normalize nullable union tool types#72
PederHP wants to merge 1 commit intoanthropics:nextfrom
PederHP:meai_fix_tool_types

Conversation

@PederHP
Copy link
Contributor

@PederHP PederHP commented Dec 12, 2025

See: modelcontextprotocol/csharp-sdk#1091

Tools with ["integer", "null"] discriminated union style types to provide additional information on default when non provided for non-required properties would cause the following error:

tools.0.custom.input_schema: JSON schema is invalid. It must match JSON Schema draft 2020-12 (https://json-schema.org/draft/2020-12). Learn more about tool use at https://docs.claude.com/en/docs/tool-use.

@PederHP PederHP requested a review from a team as a code owner December 12, 2025 17:32
@PederHP PederHP changed the title Normalize nullable union tool types MicrosoftExtensions: Normalize nullable union tool types Dec 12, 2025
@PederHP PederHP force-pushed the meai_fix_tool_types branch from f984bf5 to f0d5a56 Compare December 12, 2025 18:21
@stainless-app stainless-app bot force-pushed the next branch 2 times, most recently from a6ccfa7 to 007b0c0 Compare December 18, 2025 22:39
@PederHP
Copy link
Contributor Author

PederHP commented Jan 15, 2026

@stephentoub Any comments on this one? I made it to unbreak use of MCP Server tools which have discriminated unions from default parameters (which are valid JSON schema but not supported by the Anthropic API). I assume the code owners will lean on your review of this as it relates only to MEAI.

It would be really nice to get this in as it as otherwise the risk of MCP tools breaking the client is quite high with the beta client - requiring the use of a forked SDK. If the stricter schema validation of the beta client makes it to the main client, I think this will affect almost anyone using MCP with the SDK and extensions.

@stephentoub
Copy link
Contributor

@eiriktsarpalis, could you take a look at this one?


// Handle nullable union types (e.g., "type": ["integer", "null"]) for
// non-required properties. The C# MCP SDK generates these for optional
// parameters, but structured outputs doesn't support union types.
Copy link

@eiriktsarpalis eiriktsarpalis Jan 21, 2026

Choose a reason for hiding this comment

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

Is the form "type" : ["integer","null"] considered a union type? I would have guessed such a terminology would be reserved for schemas using "anyOf" or "oneOf". Is the trivial schema {} (which we generate for things like object or JsonElement) similarly considered a union type in that case, and would we need to similarly restrict those should they turn up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will have to test if {} causes an error. Note that the API just rejects the request without more information than shown in the PR description. Calling it a (discriminated) union type is on me. If there’s a more accurate description I am happy to change the comment.

But the API definitely rejects requests if this pattern is present.

Copy link

@eiriktsarpalis eiriktsarpalis left a comment

Choose a reason for hiding this comment

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

The fact that this is constrained to a particular chat client makes me feel less concerned, however I do think that a nullable/optional input parameter encodes important aspects of an AIFunction's contract, so silently erasing those is likely going to result in unexpected behavior.

I would much rather have the chat client fail with an explicit "not supported" error message and give the user to opportunity to model their functions in a way that remains compatible with the LLM in question.

@stephentoub
Copy link
Contributor

stephentoub commented Jan 21, 2026

I would much rather have the chat client fail with an explicit "not supported" error message and give the user to opportunity to model their functions in a way that remains compatible with the LLM in question.

Then why do we expose transforms at all? The fact is that clients are talking to any number of mcp servers they don't own, all that have varying forms of schema employed as the mcp spec says nothing about the format, and talking to llms that each have their own peculiarities. Failing is a bad option in my opinion, as it completely breaks being able to use a valid mcp server with a valid llm.

@eiriktsarpalis
Copy link

eiriktsarpalis commented Jan 21, 2026

I would much rather have the chat client fail with an explicit "not supported" error message and give the user to opportunity to model their functions in a way that remains compatible with the LLM in question.

Then why do we expose transforms at all?

Marking all inputs as required or transforming "type" : "null" to "nullable" : "true" is one thing, silently restricting the function inputs is another. The point of the transform is to do just that, transform the advertised schema in a way that remains compatible with the underlying function contract. I could write a transform that collapses all schemas to accepting just one integer, but that wouldn't make it valid transform.

@stephentoub
Copy link
Contributor

The point of the transform is to do just that, transform the advertised schema in a way that remains compatible with the underlying function contract.

Reducing the allowed inputs to a subset is still compatible. If the other excluded inputs are erroneous from the llms perspective, then it wouldn't generate those, anyway.

If there's a better way to express the transformation, great, let's do that. If there isn't, I fail to see the problem here.

@eiriktsarpalis
Copy link

Reducing the allowed inputs to a subset is still compatible. If the other excluded inputs are erroneous from the llms perspective, then it wouldn't generate those, anyway.

I'm not questioning the soundness of it, however my point is that it could be limiting the intended functionality of the passed function. Consider the following hypothetical method, which would lose the ability to enumerate all documents:

// If no owner id is specified, all documents are returned
IEnumerable<Document> GetDocuments(int? ownerId = null);

@stephentoub
Copy link
Contributor

Consider the following hypothetical method, which would lose the ability to enumerate all documents:

If the LLM doesn't support producing null for the input, the transform isn't losing anything.

If the LLM does support producing null for the input, and there's some way to express in the schema to the LLM that the parameter may be null, then the transform should produce the right schema for it.

@eiriktsarpalis
Copy link

eiriktsarpalis commented Jan 21, 2026

Consider the following hypothetical method, which would lose the ability to enumerate all documents:

If the LLM doesn't support producing null for the input, the transform isn't losing anything.

Yes? I think the point is that the user wants the LLM to possibly provide null and we're silently pushing that part of the contract under the rug. We should instead provide feedback prompting the user to remodel their function into something compatible with the constraints of the current LLM.

@stephentoub
Copy link
Contributor

stephentoub commented Jan 21, 2026

Yes? I think the point is that the user wants the LLM to possibly provide null and we're silently pushing that part of the contract under the rug.

No, we're not. We have no control over what an LLM does. An LLM that's asked for an integer might only be capable of producing the interger 42, and that's fine. The client is choosing to use that LLM. If a function says it can accept null and the LLM isn't capable of producing null, the LLM just won't produce null; that's not an error, that's implicit to the LLM being used, just as is any other aspect of how good or bad the LLM is at particular workloads. It is not our place to error out if the LLM doesn't meet a particular capability threshold.

@PederHP
Copy link
Contributor Author

PederHP commented Jan 21, 2026

I think we should be able to have a client built using the Anthropic SDK and the IChatClient extension connect to arbitrary MCP servers without getting runtime errors due to the API not accepting discriminated unions.

LLMs consider unspecified parameters as something different than null. The LLM is perfectly capable of not passing an optional int parameter.

The user can't remodel functions because they're neither the client or server developer.

Add handling for nullable union types (e.g., "type": ["integer", "null"])
generated by the C# MCP SDK for optional nullable parameters. Transforms
them to simple types for non-required properties since structured outputs
doesn't support union types.

Changes:
- Add nullable union type normalization in schema transform
- Add TransformNullableUnionType helper for tool property processing
- Reorder required/properties parsing to support transformation
- Add tests for nullable union type transformation
@PederHP PederHP force-pushed the meai_fix_tool_types branch from 06b7bf3 to 7cd5a46 Compare February 7, 2026 12:34
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.

3 participants