Fix $ref pointer resolution after output schema wrapping#1435
Fix $ref pointer resolution after output schema wrapping#1435weinong wants to merge 2 commits intomodelcontextprotocol:mainfrom
Conversation
When CreateOutputSchema() wraps a non-object schema under properties.result, internal $ref JSON Pointers emitted by System.Text.Json's JsonSchemaExporter become unresolvable because they still point to the original (unwrapped) paths. Add RewriteRefPointers() to prepend /properties/result to every $ref that starts with #/, keeping the pointers valid after wrapping. Fixes modelcontextprotocol#1434
2b469d0 to
9c68a58
Compare
There was a problem hiding this comment.
Pull request overview
Fixes structured output JSON Schema generation for tools that return non-object types by ensuring $ref JSON Pointers remain valid after the schema is wrapped under properties.result (addresses #1434).
Changes:
- Add
RewriteRefPointersto rewrite internal$refvalues (#/…and#) after non-object output schemas are wrapped underproperties.result. - Add tests that validate wrapped structured output schemas still validate and that all
$refpointers resolve.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs | Rewrites $ref JSON Pointer paths after wrapping non-object output schemas under properties.result. |
| tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs | Adds coverage for duplicated-type and recursive-type $ref scenarios to ensure rewritten pointers are resolvable post-wrapping. |
You can also share your feedback on Copilot code review. Take the survey.
Address Copilot review feedback: AssertAllRefsStartWith and AssertAllRefsResolvable now return a count of $ref nodes encountered, and both tests assert the count is > 0 so they cannot vacuously pass if JsonSchemaExporter stops emitting $ref for these shapes.
| ["required"] = new JsonArray { (JsonNode)"result" } | ||
| }; | ||
|
|
||
| // After wrapping, any internal $ref pointers that used absolute JSON Pointer |
There was a problem hiding this comment.
@stephentoub unless my knowledge of the stack is completely out of date, the encoding of method parameters as object properties happens at the AIFunctionFactory layer in Microsoft.Extensions.AI. Correspondingly, shouldn't this fix be applied in that library?
There was a problem hiding this comment.
Nevermind, I just realized this specifically applies to the transform happening literally a few lines above. I'm assuming we already do apply the same fixup in corresponding AIFunctionFactory locations?
| } | ||
| else if (node is JsonArray arr) | ||
| { | ||
| foreach (var item in arr) |
There was a problem hiding this comment.
Why does the JsonObject require snapshotting while JsonArray does not? To my knowledge, both should allocate enumerators.
Summary
CreateOutputSchema()wraps a non-object schema underproperties.result, rewrite all internal$refJSON Pointers so they remain valid at their new location.StructuredOutput_WithDuplicateTypeRefs_RewritesRefPointersto verify the fix.Problem
System.Text.Json'sJsonSchemaExporterdeduplicates repeated types by emitting$refpointers with absolute JSON Pointer paths (e.g.,#/items/properties/foo/items). WhenCreateOutputSchema()wraps a non-object schema inside{ "type": "object", "properties": { "result": <original> } }, these$refpaths become unresolvable because the original schema has moved under#/properties/result.This breaks all tools that:
IEnumerable<T>,List<T>, arrays, primitives)UseStructuredContent = true$refdeduplication)When a client calls
tools/list, the broken schema causes JSON Schema validation errors. In MCP clients that don't isolate per-tool errors (e.g., the TypeScript SDK'scacheToolMetadata()), one broken tool schema prevents all tools from being enumerated.Fix
Add
RewriteRefPointers()— a recursive walk over the wrapped JSON tree that prepends/properties/resultto every$refvalue starting with#/. Called immediately after the wrapping block inCreateOutputSchema().Reproduction
A minimal repro is available as a gist: https://gist.github.com/weinong/7a750e9b99c846dadc4fa41fc6c856fc
Fixes #1434