Review fixes for #189: detect input-required prompt/resource results - #190
Merged
JAORMX merged 1 commit intoJul 25, 2026
Conversation
Review fixes for #189. The substantive one is a silent-wrong-result bug; the rest are doc corrections and test hardening. WithoutMultiRoundTrip opts out of go-sdk's automatic MRTR handling for all three MRTR-capable methods -- go-sdk's clientMultiRoundTripMiddleware intercepts tools/call, prompts/get AND resources/read -- but only CallToolResult modeled the "resultType" classification. CallTool works because it converts via jsonConvert; convertGetPromptResult and convertReadResourceResult are hand-written field-by-field (their Content / ResourceContents interface fields cannot survive a JSON round-trip), so they dropped resultType before the caller could ever see it. Reproduced before the fix: GetPrompt against a stateless 2026-07-28 backend whose handler returns InputRequests yields err == nil and zero Messages, indistinguishable from an empty completed result. The draft spec lists all three operations as returning InputRequiredResult, and go-sdk's own MultiRoundTripOptions.Disabled doc names all three NeedsInput methods as the caller's contract. - Add NeedsInput to GetPromptResult and ReadResourceResult, mirroring CallToolResult, and populate it in both converters from go-sdk's NeedsInput(). Both the live and isResume() paths route through the converters, so both are covered. - Correct the HEADER_MISMATCH doc: it described matching against "the initial InitializeRequest", but 2026-07-28 is stateless with no handshake. -32020 is validation of headers against the same request's own body. go-sdk's own comment (shared.go:386) already had this right. - Order the MCP error-code block numerically and mark -32002 and -32042 as legacy: the draft spec says implementations of this revision MUST NOT emit either, though clients SHOULD still accept -32002 from older servers. - Note on NeedsInput that a present-but-unrecognized resultType also reports false, which the spec treats as invalid rather than complete. Documented rather than fixed: go-sdk's NeedsInput is the identical comparison and the shim mirrors it, so diverging would make the shim reject what go-sdk accepts. - Correct WithoutMultiRoundTrip's rationale: go-sdk reads ClientOptions.MultiRoundTrip in NewClient (which Initialize calls), not in Connect. "Must be set before Initialize" was right for the wrong reason. - Add a regression test that CallToolResult.MarshalJSON does not re-emit resultType. The invariant was documented and honored but untested; a proxy that unmarshals, mutates and re-marshals a result would silently emit a stale classification if it broke. - Strengthen "default MRTR errors without a fulfilling handler" from a bare assert.Error to pin the actual contract (*jsonrpc.Error, code -32602, "client does not support elicitation"), so a transport EOF no longer satisfies it. - Qualify the cross-repo issue reference as stacklok/toolhive#5911. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 tasks
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.
Stacked on top of #189 (base is
feat/mcpcompat-modern-client-surface, notmain) so @JAORMX keeps control of the merge and I keep my review on #189. Merge or cherry-pick at your discretion; if you'd rather these were squashed into your commit, say so and I'll rebase them in.The one that matters: input-required prompt/resource results were undetectable
WithoutMultiRoundTrip()opts out of go-sdk's automatic MRTR handling for all three MRTR-capable methods — go-sdk'sclientMultiRoundTripMiddlewareinterceptstools/call,prompts/getandresources/read(mrtr.go:76-79), and its ownMultiRoundTripOptions.Disableddoc names all threeNeedsInputmethods as the caller's contract. But onlyCallToolResultmodeledresultType.CallToolworks because it converts viajsonConvert.convertGetPromptResult/convertReadResourceResultare hand-written field-by-field (theirContent/ResourceContentsinterface fields can't survive a JSON round-trip), so they droppedresultTypebefore the caller could see it.Reproduced before the fix —
GetPromptagainst a stateless 2026-07-28 backend whose handler returnsInputRequests:err == nil, zero messages, no way to tell it apart from an empty completed result. The draft spec's MRTR page lists all three operations as returningInputRequiredResult.Fix:
NeedsInputonGetPromptResultandReadResourceResultmirroringCallToolResult, populated in both converters from go-sdk'sNeedsInput(). The live andisResume()paths both route through the converters, so both are covered (go-sdk's ownUnmarshalJSONpopulatesresultTypeon the resume path before the converter runs).Doc corrections
HEADER_MISMATCHsaid headers must match "the initialInitializeRequest". 2026-07-28 is stateless with no handshake;-32020validates headers against the same request's own body. go-sdk's comment (shared.go:386) already had this right — the shim reworded it and lost the meaning.-32002/-32042are marked legacy: the draft spec says implementations of this revision MUST NOT emit either, though clients SHOULD still accept-32002from older servers. They were sitting unmarked next to the three spec-current codes.NeedsInputcaveat. A present-but-unrecognizedresultTypealso reportsfalse, which the spec requires clients to treat as invalid, not complete. Documented rather than fixed — go-sdk'sNeedsInputis the identical comparison, and diverging would make the shim reject what go-sdk accepts. Flagging in case you want to raise it upstream.WithoutMultiRoundTriprationale. go-sdk readsClientOptions.MultiRoundTripinNewClient(whichInitializecalls), not inConnect. "Must be set before Initialize" was right for the wrong reason.#5911→stacklok/toolhive#5911.Test hardening
MarshalJSONnot re-emittingresultTypewas documented and honored but untested. Now guarded — a proxy that unmarshals/mutates/re-marshals a result would otherwise silently emit a stale classification if it broke.default MRTR errors without a fulfilling handlerasserted onlyassert.Error, which a transport EOF would satisfy. Now pins*jsonrpc.Error/-32602/"client does not support elicitation".Open question for you — not decided here
GetPromptResult/ReadResourceResultneeded some way formcpcompat/clientto set a classification that lives in packagemcp. This PR uses an unexportedresultType+ exportedSetNeedsInput(bool), matching the shape you chose onCallToolResult.One reviewer argued for an exported
ResultType stringtaggedjson:"-"plus exportedResultTypeComplete/ResultTypeInputRequiredconsts instead, deleting both setters. The case for it:SetNeedsInput(true)on a server-built result compiles, reads as meaningful, and does nothing —MarshalJSONdrops the field. An imperative verb that makes no observable change is a footgun.json:"-"on an exported field is exactly as wire-safe (encoding/jsonnever touches unexported fields either way), and this package already does that in three places:RawInputSchema,RawOutputSchema,Extra.boolpermanently collapses the spec's open string union; the exported field would surface the raw string on theCallToolResultpath where it actually exists, and let the "not otherwise exposed" caveat be deleted.Counter-argument: some cross-package channel is structurally unavoidable, and the bool loses nothing go-sdk exposes publicly anyway. Roughly +25/−40 and mechanical if you want it, but it changes your
CallToolResultfield too, so it's your call, not mine. Happy to push it to this branch if you'd like it.Verification
go build ./...,go test -race ./mcpcompat/...,task lint(0 issues),go vet,task license-check— all clean. Both new tests were confirmed to fail when the corresponding fix is reverted. Unrelated:TestOnConnectionLost_RegisterAfterInitializeflaked once under full-package load (aCloseIdleConnectionstransport race inconn_lost_test.go, untouched here) — pre-existing, worth a separate look.🤖 Generated with Claude Code