Relax outputSchema to any JSON Schema 2020-12 document per SEP-2106#1568
Conversation
…o any JSON Schema 2020-12 document per SEP-2106
|
can you help review this PR |
| } | ||
|
|
||
| private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions, out bool structuredOutputRequiresWrapping) | ||
| private static JsonElement? CreateOutputSchema(AIFunction function, McpServerToolCreateOptions? toolCreateOptions) |
There was a problem hiding this comment.
For backward compatibility reasons, I don't think we can completely eliminate the wrapping of non-object schemas, since this will still be needed for clients using 2025-11-25 or earlier protocol versions.
There was a problem hiding this comment.
wrapping for non-object schemas is restored in c30725c, just at the wire emission boundary instead of inside CreateOutputSchema. The classification + transformation logic is the same as before SEP-2106; it moved from registration-time storage to per-request emission so a single registered Tool can serve both legacy and SEP-2106 clients off the same server
| // with keywords like "type", "properties", etc.) or a boolean (`true` matches anything, | ||
| // `false` matches nothing). Stricter keyword-level validation is intentionally not | ||
| // performed. | ||
| internal static bool IsValidJsonSchemaDocument(JsonElement element) => |
There was a problem hiding this comment.
I think this method should be named IsValidToolOutputSchema, since it is designed specifically to validate tool output schemas and not any JSON Schema document.
I think we may also need to make this conditional on the protocol version.
There was a problem hiding this comment.
Renamed the function.
About version control here, I did not make that change to keep tools contain schema per SEP-2106. But serving of schema on tools/call and tools/list make the decision of wrapping depending on negotiated protocol version
mikekistler
left a comment
There was a problem hiding this comment.
Thank you for this contribution!
This generally looks good but I think we may need to retain the behavior of prior protocol versions for backward compatibility.
|
I will make a change to consider negotiated protocol version |
…hape and rename outputSchema validator
- Wire-side back-compat at tools/list and tools/call: clients negotiating a
protocol version older than 2026-06-30 still receive the
{type:object,properties:{result:<schema>}} envelope for non-object
output schemas (and type:[object,null] normalized to object); 2026-06-30+
clients receive the natural JSON Schema 2020-12 document. Gated per-session
via McpSessionHandler.SupportsNaturalOutputSchemas. In-memory schema storage
stays natural per SEP-2106.
- Rename McpJsonUtilities.IsValidJsonSchemaDocument → IsValidToolOutputSchema.
Tool.OutputSchema exception message and XML doc updated to reflect that
JSON Schema 2020-12 boolean schemas (true/false per §4.3) are also accepted.
- 10 new Theory tests across the two wire emission boundaries:
- tests/.../McpServerToolTests.cs: 5 Theories for tools/call structuredContent
value wrapping (string/int/array/object/nullable object × legacy/DRAFT/new
protocol versions) plus a pinned existing invocation test.
- tests/.../Sep2106ListToolsBackCompatTests.cs (new): 5 Theories for
tools/list outputSchema shaping across the same protocol-version axis
|
Thanks @mikekistler ! I tried to fix your comments on c30725c. What clients now see
How
Why Validator rename
Verification
Please help review the changes |
feat: relax outputSchema to any JSON Schema 2020-12 document per SEP-2106
Summary
Brings the C# SDK into compliance with SEP-2106 — Tools
inputSchema&outputSchemaConform to JSON Schema 2020-12Closes #1550
The SDK previously wrapped any non-object output schema in a
{"type":"object","properties":{"result":<schema>},"required":["result"]}envelope at registration, and wrapped the corresponding return value in{"result": <value>}at every invocation. Both wrappings exist because the older MCP spec requiredoutputSchema.type == "object". SEP-2106 lifts that restriction. This PR removes both ends of the wrapping plumbing, relaxes the validator, and lets natural JSON shapes flow through.What changes on the wire
int Foo() => 72;{"type":"object","properties":{"result":{"type":"integer"}},"required":["result"]};structuredContent{"result":72}{"type":"integer"};structuredContent72string Foo() => "Paris";{"result":"Paris"}"Paris"string[] Foo() => […];{"result":[…]}[…]Person? Foo() => null;{"result": null}nullPerson Foo() => new(...);{"name":…,"age":…}(already an object — unchanged){"name":…,"age":…}(unchanged)inputSchemais not changed — tool inputs are still required to havetype: "object", which is consistent with SEP-2106 (it loosensoutputSchemaonly).Source-level changes
src/ModelContextProtocol.Core/McpJsonUtilities.cs— addedIsValidJsonSchemaDocumentaccepting JSON object or boolean. KeptIsValidMcpToolSchemaunchanged forInputSchema(still strict).src/ModelContextProtocol.Core/Protocol/Tool.cs—OutputSchemasetter switched toIsValidJsonSchemaDocument; XML doc rewritten to reference SEP-2106 and the new contract.src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs— deleted the_structuredOutputRequiresWrappingfield, theout boolparameter onCreateOutputSchema, the entire wrapping/normalization block inCreateOutputSchema, the wrapping block inCreateStructuredResponse, and constructor wiring around the flag.Contract widening
The
IsValidJsonSchemaDocumentvalidator accepts a slightly broader set than just JSON objects. The full acceptance/rejection table:OutputSchemavalue{"type":"object","properties":…}{"type":"array","items":…}{"type":"string"},{"type":"integer"}, etc.{"type":["object","null"], …}{}{"oneOf":[…]}typetruefalsenullliteralArgumentExceptionArgumentExceptionBehavioural break call-out
Source-breaking for tools that returned non-object types and whose downstream consumers relied on the
{"result": …}envelope. After this PR the same tool emits the raw value. Per SEP-2106 §"Backward Compatibility": servers that emit array/primitivestructuredContentand want to stay interoperable with old clients SHOULD also emit aTextContentblock with the serialized JSON.The SDK doesn't auto-add such a block, that remains the tool author's choice.
End-to-end smoke test
Beyond unit tests, I built a small in-process client/server demo against this branch that prints both phases of MCP exchange (advertising and invocation) for tools whose return types span the interesting shapes —
int,string,string[], nullable record, plain record. Side-by-side before/after tables of what came out are captured inpost-2106.mdof the demo repo.dotnet run):Post-2106.mdThe demo wires up
McpServerandMcpClientover aSystem.IO.Pipelinespipe, registers tools viaMcpServerTool.Create(...)against this branch's<ProjectReference>, and printsoutputSchemafromtools/listfollowed bystructuredContentfrom eachtools/call. It confirms the wire shape end-to-end against a real client, not just unit-test mocks.Did not implement
inputSchemavalidation - explicitly unchanged per SEP-2106.IsValidJsonSchemaDocumentis intentionally only a structural check (object or boolean). No$refresolution, nooneOf/anyOfkeyword validation.TextContentmirror of structured content for old-client interop - per SEP-2106 that's a server-author choice, not an SDK default.Test plan
dotnet test tests/ModelContextProtocol.Tests(net10.0) — 1904/0/5.jayaraman-venkatesan/sep-2106-demo) shows expected wire shapes.