Skip to content

fix(core): disambiguate task result responses - #913

Open
jstar0 wants to merge 1 commit into
modelcontextprotocol:mainfrom
jstar0:fix/task-result-deserialization
Open

fix(core): disambiguate task result responses#913
jstar0 wants to merge 1 commit into
modelcontextprotocol:mainfrom
jstar0:fix/task-result-deserialization

Conversation

@jstar0

@jstar0 jstar0 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix task response deserialization so tasks/result preserves the original result object even when its shape overlaps another known result type, while tasks/get and tasks/cancel continue to return typed task state.

Root Cause

JSON-RPC responses contain an ID and result but not the original request method. The response serializer therefore classifies RequestResult from JSON shape alone. That is insufficient for task APIs: tasks/result returns the original request's result object, so a valid payload can look like GetTaskResult, CallToolResult, or an extension-defined result.

The payload could consequently be decoded into the wrong runtime subtype or rejected before Protocol.request() correlated it with the request method.

Changes

  • Preserve the raw JSON-RPC response result during message decoding.
  • Resolve tasks/get, tasks/cancel, and tasks/result using the correlated request method before completing the request.
  • Re-encode directly constructed transport responses when raw wire JSON is unavailable.
  • Keep the existing public request API and generated JSONRPCResponse serializer unchanged.
  • Add regressions for task-state-shaped, tool-shaped, unknown, custom-method, and direct-transport task results, plus JSON-RPC message serialization compatibility.

Scope and Risk

The change is limited to task response result selection. Existing non-task methods keep their current content-based result type. Unknown object-shaped responses can now reach request correlation as raw task payloads; the original result is returned unchanged only for tasks/result.

GetTaskPayloadResult is currently object-backed, so array and scalar task payloads remain outside this change.

The existing request API erases T, so its final internal completion cast remains. This change makes the task result's runtime subtype correct before that boundary. Removing the cast itself would require changing the public request contract, which was the breaking approach rejected in #717.

Verification

./gradlew --no-parallel :kotlin-sdk-core:jvmTest :kotlin-sdk-core:apiCheck :kotlin-sdk-core:ktlintCheck :kotlin-sdk-core:detekt
./gradlew --no-parallel :kotlin-sdk-core:jsTest :kotlin-sdk-core:wasmJsTest
./gradlew --no-parallel :kotlin-sdk-core:macosArm64Test :kotlin-sdk-core:iosSimulatorArm64Test

Closes #601

@devcrocod
devcrocod force-pushed the fix/task-result-deserialization branch from e29d74b to 2b11a43 Compare July 27, 2026 12:53
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.

type-unsafe cast in Protocol result deserialization

1 participant