Repository navigation
fix(server): omit non-object tool output for legacy peers - #1340
Open
Cassian433 wants to merge 1 commit into
Open
Cassian433 wants to merge 1 commit into
Cassian433 wants to merge 1 commit into
Conversation
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
Since SEP-2106 landed (#895, #933), a
Json<Vec<T>>tool advertises an arrayoutputSchemaand returns an arraystructuredContentto every client, including ones that negotiated 2025-06-18 or 2025-11-25. Both of those schema versions pinoutputSchema.typeto"object"and typestructuredContentas an object, so a client that validatestools/listagainst its negotiated version rejects the tool.handle_requestinhandler/server.rsalready stripsresultTypefor these peers. The output relaxation was never gated the same way.With the repro from the issue on main (c1a8eea),
initializeat 2025-06-18 and thentools/list+tools/call:On this branch:
I added
ServerResult::strip_non_object_output_for_legacy_peernext tostrip_result_type_for_legacy_peerand call it from the same branch for peers below 2026-07-28. It drops a tool'soutputSchemawhen the roottypeisn't the string"object", which also covers theoneOfandanyOf+$refroots schemars emits for tagged enums andOption<S>. It dropsstructuredContentwhen it isn't an object. Object schemas and object results pass through as before. The text block thatJson<T>always emits stays. If a hand-built result has no content at all, the dropped value goes into a text block so nothing is lost. TheJson<T>docs now spell out the version split.Fixes #1337
How Has This Been Tested?
The new
tests/test_structured_output_version.rsserves aJson<Vec<String>>tool and aJson<struct>tool overserve_directly, the same waytest_result_type_version.rsdoes. At 2025-06-18 and 2025-11-25 the array tool comes back with nooutputSchemaand nostructuredContent, andinto_typed::<Vec<String>>()still decodes it from the text block. The object tool keeps both. At 2026-07-28 the array schema and["a","b"]come through unchanged. On main the two legacy tests fail, with the assertion printing the"type": "array"schema. The 2026-07-28 test passes either way.Three unit tests in
model.rspin the rules: array,oneOfandanyOf+$refschemas are dropped while an object root is kept, an emptycontentgets the value as text, and an existing text block isn't duplicated. Against the strip without the empty-content fallback, the second one fails withcontentcoming back as[].I also ran the issue's stdio repro against main and this branch. Asking for 2026-07-28 in
initializestill negotiates 2025-11-25, and that session now gets the stripped list too. A 2026-07-28 session using inline_metaproduces byte-identical output before and after (479 bytes both ways).cargo test -p rmcpwith every feature exceptlocalpasses 1321 tests,cargo test --all-featurespasses 909, andcargo test -p rmcp --lib --no-default-featurespasses 142. Both CI clippy invocations,cargo +nightly fmt --all -- --check, the nightly rustdoc build andcargo +1.88.0 checkare clean.Breaking Changes
Only for peers below 2026-07-28. Code that reads
structured_contentdirectly for a list-returning tool on such a session now getsNoneand has to use the text block;CallToolResult::into_typedalready falls back to it. 2026-07-28 sessions see no change.Types of changes
Checklist
Additional context
I went with omitting instead of wrapping. The TypeScript SDK (modelcontextprotocol/typescript-sdk#2337) and the C# SDK wrap for 2025-era peers: the schema becomes
{"type":"object","properties":{"result":...}}and the value becomes{"result": value}. That only works if the value-side wrap matches the schema-side wrap, andhandle_requestonly sees theCallToolResult, not the tool it came from. Matching them would mean threading the tool throughToolRouter, and hand-writtencall_toolimpls would still go unmatched. Dropping both sides is stateless and always valid against the 2025 schemas. An opt-in wrapper (the issue's second suggestion) can come later.Router<S>answerstools/listand routedtools/callitself and never reaches this branch, the same way it already skips theresultTypestrip and the cache hints. I left that for a follow-up.