-
Notifications
You must be signed in to change notification settings - Fork 1.1k
SEP-1905:Task Result Streaming and Immediate Result Acceptance #1905
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@LucaButBoring FYI, feel free to push this forward and add yourself as the author The core purpose is to support:
|
|
I think this should be split into separate SEPs for the immediate result mode and the streaming mode, if you have no objections to that (partial results has been a longtime thing we've wanted to build on tasks - I don't mind writing an SEP for that based loosely on this). I also think this proposal is too specific to tools - part of the reason I made sure elicitation and sampling were included in the base tasks spec was explicitly to avoid rushing to implement tasks (or extensions to tasks) in a tool-specific way. In particular, streaming content blocks (as opposed to some other data type) only actually makes sense in two of the JSON-RPC operations today that tasks supports: tools and sampling. In the case of both, there are more fields besides In the case of the immediate result mode, the client implementation guide also only actually works for tools, since those are the only server result type that typically has a Finally, I think reusing the same request ID for all parts of a streamed response would be a new violation of JSON-RPC, which is something I would expect to be very difficult to push through, though it may not necessarily be a dealbreaker. |
|
cc: @mikekistler |
|
Received. I also think that mixing streaming and real-time results in one SEP would be too complicated. I will separate them at a later time. |
@LucaButBoring
|
SEP-1905: Task Result Streaming and Immediate Result Acceptance
Status: Draft
Type: Standards Track
Created: 2025-11-20
Author(s): He-Pin hepin.p@alibaba-inc.com (@He-Pin)
Sponsor: TBD
PR: TBD
Abstract
This SEP proposes a task result transmission mechanism that allows clients to accept task results immediately as they
are produced by the server, rather than waiting for the entire task to complete.
This approach aims to reduce latency and improve responsiveness in scenarios where tasks generate results incrementally.
Motivation
In real-world applications, tasks often produce results in a streaming fashion. For example, data processing tasks may
generate intermediate results that can be consumed as they become available. By enabling immediate result acceptance,
clients can start processing these results sooner, leading to faster overall workflows.
Otherwise, the clients have to wait at least one
pollIntervalto get the results after the task is finished, which may introduce unnecessary delays.Specification
Capability
Server and clients that support this SEP MUST indicate their capability during the initial handshake or capability
negotiation phase.
Server Capabilities
tasks.responses.modes{ "capabilities": { "tasks": { "responses": { "modes": ["task", "immediate", "streaming"] } } } }task: The server supports traditional task result retrieval, where results are provided after task completion.immediate: The server supports immediate result responding, MAY send the immediately available result to clients when results are ready at request time.streaming: The server supports streaming of task results, enabling clients to receive results incrementally as they are produced.Client Capability
tasks.responses.modes{ "capabilities": { "tasks": { "responses": { "modes": ["task", "immediate", "streaming"] } } } }task: The client supports traditional task result retrieval, where results are provided after task completion.immediate: The client supports immediate result acceptance, MAY accept immediately available results from servers.streaming: The client supports streaming of task results, enabling real-time incremental result reception.Capability Negotiation
During the capability negotiation phase, both the server and client MUST exchange their supported capabilities. When starting a task-augmented request, the client MUST specify the supported response modes with
responseModesin thetaskfield of the request parameters.responseModeslist that the server also supports. The server MUST NOT use a response mode that is not in the client's specified list, even if the server supports it.immediate,streaming,task.taskmode.taskmode (even iftaskis not in the client'sresponseModeslist, astaskis the default fallback mode) and SHOULD include a warning in the response indicating the fallback. The warning MAY be included in the_metafield of the response (e.g.,_meta["io.modelcontextprotocol/fallback-mode"] = "task") or in astatusMessagefield if a task is created.Task Result Transmission
When a client makes a task-augmented request, the server MUST respond according to the negotiated response mode.
Server Response Decision Process:
When processing a task-augmented request, the server MUST follow this decision process:
Determine the negotiated response mode:
responseModesin the request, select a mode from that list that the server also supports (prioritizingimmediate>streaming>task)responseModesis specified, default totaskmodetaskmodeCheck result availability and respond accordingly:
immediateAND complete results are available immediately: MAY respond with immediate full result (Case 1)immediatebut results are not immediately available: The server MUST determine whether results will be produced incrementally:streamingmode is supported by both client and server: MUST fall back tostreamingmode (Case 3)taskmode (Case 2)taskOR (negotiated mode isimmediatebut fallback totask): MUST returnCreateTaskResult(Case 2)streamingAND results are produced incrementally: MUST create a task and MAY send streaming results (Case 3)Request Example:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "get_weather", "arguments": { "city": "New York" }, "task": { "ttl": 60000, "responseModes": ["immediate", "task", "streaming"] } } }Case 1: Responding with Immediate Full Result
When the negotiated response mode is
immediateAND the server has complete results available immediately after processing the request,it MAY respond with the full results directly in the response. In this case:
completedimmediately if a task is created.tasks/resultto retrieve the result, which MUST return the same result (using thecontentfield format).io.modelcontextprotocol/related-taskmetadata in_metaif and only if a task was created. If no task is created, the_metafield MAY be omitted or MAY be included without therelated-taskkey.immediatemode, the server MUST follow the negotiated mode (e.g., return aCreateTaskResultfortaskmode, or start streaming forstreamingmode).contentfield (notpartial-content) to return the complete results, matching the format of standard tool results.Response (Results already available):
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" }, { "type": "text", "text": "Suggested activities:\n- Visit Central Park\n- Explore the Metropolitan Museum of Art" }, { "type": "video", "text": "Here is a short video overview of New York City attractions.", "url": "https://example.com/nyc_overview.mp4" } ], "isError": false, "_meta": { "io.modelcontextprotocol/related-task": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" } } } }Case 2: Responding with Task
When the server does not have results available immediately or the negotiated mode is
task, it MUST return aCreateTaskResultas specified in the existing tasks specification. The client MUST then usetasks/getto poll for status andtasks/resultto retrieve the final result.Response (Results not yet available):
{ "jsonrpc": "2.0", "id": 1, "result": { "task": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", "status": "working", "statusMessage": "The operation is now in progress.", "createdAt": "2025-11-25T10:30:00Z", "lastUpdatedAt": "2025-11-25T10:40:00Z", "ttl": 60000, "pollInterval": 5000 } } }Case 3: Responding with Streaming Results
When the negotiated response mode is
streamingAND results are produced incrementally, it MUST create a task. The server MAY then send streaming results through one or more of the following mechanisms:partial-contentMUST be sent as JSON-RPC responses (not notifications), as notifications do not have aresultfield.tasks/resultto retrieve additional segmentsNote: The choice of mechanism depends on the transport type and server implementation. The client MUST be prepared to handle results from any of these mechanisms. All mechanisms for the same task MUST maintain consistent
seqNrordering.SSE Stream Message Format:
When using SSE stream for streaming results, the server MUST send JSON-RPC responses (not notifications) in the SSE stream. Each SSE event's
datafield MUST contain a complete JSON-RPC response message. For streaming task results, these responses MUST follow the same format as other streaming responses, containingpartial-contentarrays withseqNrvalues in theresultfield.Note: While SSE streams may carry JSON-RPC notifications for other purposes (e.g., task status updates via
notifications/tasks/status), streaming task results withpartial-contentMUST be sent as JSON-RPC responses because:resultfieldpartial-contentmust be included in theresultfield of a JSON-RPC responseidfield in the response allows correlation with the original requestExample SSE event for streaming result:
The client MUST parse each SSE event's
datafield as a JSON-RPC response message and extractpartial-contentsegments from theresultfield accordingly. Thejsonrpcidin SSE responses for streaming results MUST match the original requestid.Note: If the client receives JSON-RPC notifications in the SSE stream (e.g.,
notifications/tasks/status), these should be handled separately and do not containpartial-content. Only JSON-RPC responses withresult.partial-contentcontain streaming segments.Client Handling of Multiple Mechanisms:
When the server uses multiple mechanisms simultaneously (e.g., initial response + SSE stream, or SSE stream + tasks/result), the client MUST:
seqNrvalues for each task to detect duplicatesseqNrthat has already been received, regardless of which mechanism delivered itseqNrorder, removing duplicatesseqNrare detected, the client MAY usetasks/resultwithlastSeqNrto request missing segmentsImportant: The same
seqNrvalue MUST NOT appear in multiple mechanisms for the same task. If a server sends the same segment through multiple mechanisms, it MUST use differentseqNrvalues or ensure only one mechanism delivers each segment.Initial Response Options:
The server MAY choose one of the following approaches for the initial response:
Return both task and partial-content: Include both the
taskobject (as inCreateTaskResult) andpartial-contentin the same response if partial results are immediately available.Example:
{ "jsonrpc": "2.0", "id": 1, "result": { "task": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", "status": "working", "ttl": 60000, "pollInterval": 5000 }, "partial-content": [ { "seqNr": 1, "type": "text", "text": "Initial result segment" } ], "isError": false, "isComplete": false } }Return only task: Return a standard
CreateTaskResultwith thetaskobject, then send streaming results in subsequent responses (same as Case 2 format).Return only partial-content: Return only
partial-contentin the initial response (as shown in the example below), indicating that a task has been created via therelated-taskmetadata.The task status MUST remain
workinguntilisComplete: trueis sent, after which it MUST transition tocompleted.Initial Response Example (Partial results available immediately, task created):
The server may return only
partial-contentin the initial response, with the task ID inrelated-taskmetadata:{ "jsonrpc": "2.0", "id": 1, "result": { "partial-content": [ { "seqNr": 1, "type": "text", "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" }, { "seqNr": 2, "type": "text", "text": "Suggested activities:\n- Visit Central Park\n- Explore the Metropolitan Museum of Art" } ], "isError": false, "isComplete": false, "_meta": { "io.modelcontextprotocol/related-task": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" } } } }Subsequent Streaming Response (More partial results):
{ "jsonrpc": "2.0", "id": 1, "result": { "partial-content": [ { "seqNr": 3, "type": "video", "text": "Here is a short video overview of New York City attractions.", "url": "https://example.com/nyc_overview.mp4" } ], "isError": false, "isComplete": true, "_meta": { "io.modelcontextprotocol/related-task": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" } } } }To support streaming results, the server MUST include the following fields in each streaming response:
partial-content: An array of result segments. Each segment MUST be a validContentBlock(as defined in the MCP specification) with an additional requiredseqNrfield. The structure of each segment is the same as items in thecontentarray used in standard tool results, but withseqNradded to indicate ordering.isComplete: A boolean indicating whether the task has completed and all results have been sent.Note: The
partial-contentsegments have the same structure ascontentitems (e.g.,{type: "text", text: "..."}or{type: "image", data: "...", mimeType: "..."}), but each segment MUST include aseqNrfield to indicate its position in the sequence. When combining segments into a final result (e.g., viatasks/resultwithoutlastSeqNr), theseqNrfields are removed and segments are merged into a standardcontentarray.Sequence Number Requirements:
seqNrvalues starting from 1.jsonrpcidfor correlation when sent as part of the original request-response flow. However, when usingtasks/resultto resume streaming, the response uses theidfrom thetasks/resultrequest (not the original requestid).seqNrMUST be monotonically increasing within a task, with no duplicates.lastSeqNr. When resuming withlastSeqNr, the server returns segments starting fromlastSeqNr + 1(i.e., the first segment after the one specified bylastSeqNr).seqNrMUST be a positive integer (1, 2, 3, ...).Streaming Behavior:
partial-contentuntil the task is complete.seqNrusingtasks/resultwithlastSeqNr.isComplete: truein the next response and transition the task to a terminal status (completed,failed, orcancelled)tasks/getto detect TTL expiration and handle it gracefullytasks/result(if the task is still accessible)isError: trueand MUST transition the task tofailedstatus. The final response MUST follow these format rules:partial-contentformat and include those partial results (with theirseqNrvalues)contentformat with the error messageisCompletefield MUST be set totruein all error responsesisErrorfield MUST be set totruein all error responsescancelledstatus. If a final response is sent, it MUST haveisComplete: trueand MAY include any partial results generated before cancellation.Resuming Streaming
If the client wants to resume streaming from a specific sequence number, it MUST include the
lastSeqNrfield in the parameters of thetasks/resultrequest.Request:
{ "jsonrpc": "2.0", "id": 4, "method": "tasks/result", "params": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840", "lastSeqNr": 2 } }Error Handling:
lastSeqNris invalid (e.g., negative, zero, or non-numeric), the server MUST return a-32602(Invalid params) error. Note: SinceseqNrvalues start from 1,lastSeqNrof 0 or less is invalid. ValidlastSeqNrvalues are positive integers (1, 2, 3, ...).lastSeqNrequals the highest sequence number available:partial-contentarray withisComplete: true.partial-contentarray withisComplete: false(indicating no new segments since that sequence number, but more may come).lastSeqNris greater than the highest sequence number available, the server MUST return all remaining segments starting fromlastSeqNr + 1(which may be an empty array if streaming is complete). For example, if the highestseqNris 5 andlastSeqNris 7, the server MUST return an emptypartial-contentarray withisComplete: true.seqNrstarting from 1) or return a-32603(Internal error) indicating that resumption is not supported.tasks/resultis called withlastSeqNr, the server MUST return all remaining segments (if any) or an emptypartial-contentarray withisComplete: true.Response (Resuming from seqNr 2):
{ "jsonrpc": "2.0", "id": 4, "result": { "partial-content": [ { "seqNr": 3, "type": "video", "url": "https://example.com/nyc_overview.mp4", "text": "Here is a short video overview of New York City attractions." } ], "isError": false, "isComplete": true, "_meta": { "io.modelcontextprotocol/related-task": { "taskId": "786512e2-9e0d-44bd-8f29-789f320fe840" } } } }Note: The response
id(4) matches the requestid(4), not the original streaming responseid(1), since this is a newtasks/resultrequest.Relationship to Existing Tasks Specification
This SEP extends the existing tasks specification with additional response modes. The following relationships apply:
Task Creation:
immediatemode, a task MAY still be created for tracking, but results are provided immediately.streamingmode, a task MUST be created and follows the normal task lifecycle.taskmode, behavior is unchanged from the existing specification.Task Status:
immediatemode that return results immediately MUST have statuscompleted.streamingmode MUST remain inworkingstatus untilisComplete: true, then transition tocompleted.tasks/result Operation:
tasks/resultoperation continues to work as specified in the tasks specification.tasks/resultMAY be used to resume streaming by providinglastSeqNr. In this case, it returnspartial-contentformat.lastSeqNr,tasks/resultMUST block until the task reaches a terminal status, then return the complete result. The complete result MUST be returned in the standardcontentfield format (notpartial-content), with all segments combined in order. The segments frompartial-contentMUST be merged into a singlecontentarray following these steps:partial-contentarrays received for the taskseqNrvalues in ascending order (1, 2, 3, ...)seqNrfield from each segmentcontentarray, maintaining the sorted ordercontentarray MUST contain all segments in the correct sequenceIf the task failed or was cancelled, the result MUST follow the standard error format (with
isError: trueif applicable).tasks/resultMUST return the same result that was provided in the initial response (usingcontentfield format).tasks/get Operation:
tasks/getoperation continues to work as specified, allowing clients to poll task status.tasks/getto monitor status while receiving streaming results.Backward Compatibility
New Client + Old Server:
taskmode.immediateorstreamingmodes will be used.Old Client + New Server:
taskmode whenresponseModesis not specified.Security Considerations
Access Control:
taskIdinrelated-taskmetadata MUST be validated against the requestor's authorization context.Sequence Number Security:
lastSeqNrrequests come from authorized requestors.tasks/resultrequests withlastSeqNrto prevent enumeration attacks.Data Integrity:
seqNrvalues to detect missing or duplicate segments.seqNrvalues for the same task through any mechanism.Client Implementation Guide
This section provides guidance for clients implementing support for this SEP.
Handling Different Response Modes
Immediate Mode (
immediate):contentfield (notpartial-content)contentis present, process the complete result immediately_meta["io.modelcontextprotocol/related-task"]is present, a task was created for tracking; the client MAY calltasks/resultto retrieve the same result laterTask Mode (
task):taskobjecttaskIdand usetasks/getto poll for statustasks/resultto retrieve the final resultcontentformat (standard format)Streaming Mode (
streaming):partial-contentor ataskobject (or both)taskIdfrom either thetaskobject or_meta["io.modelcontextprotocol/related-task"]seqNrvaluesseqNrisCompleteto know when streaming is finishedtasks/getto monitor task statusHandling Streaming Results
Segment Tracking:
Clients MUST maintain a data structure for each streaming task to track:
taskId: The task identifierreceivedSeqNrs: A set of receivedseqNrvalues (to detect duplicates)highestSeqNr: The highestseqNrreceived so farsegments: A map or array of segments keyed byseqNr(for ordering)isComplete: Whether streaming is completeProcessing Flow:
Initial Response:
partial-contentis present, extract segments and add to trackertaskobject is present, extracttaskIdand start polling statuspartial-contentis present, extracttaskIdfrom_meta["io.modelcontextprotocol/related-task"]SSE Stream (if using Streamable HTTP transport):
datafield as a JSON-RPC messageresultfield) andresult.partial-contentexists, extract segments fromresult.partial-contentand add to tracker (checking for duplicates)idfield), handle it separately (e.g., task status updates) - these do not contain streaming segmentsisComplete: trueis received in a response'sresultfield, mark streaming as completetasks/result Polling (if needed):
tasks/resultwithlastSeqNrset to the highest receivedseqNrisComplete: trueis receivedSegment Merging:
tasks/resultis called withoutlastSeqNr), merge segments:seqNrin ascending orderseqNrfield from each segmentcontentarrayError Handling:
seqNrare detected, usetasks/resultwithlastSeqNrto request missing segmentsseqNrvalues that have already been receivedtasks/resultwithlastSeqNrto resume from the last received segmenttasks/getindicates the task has expired, treat streaming as incomplete and attempt to retrieve available partial resultsServer Implementation Guide
This section provides guidance for servers implementing support for this SEP.
Response Mode Selection:
responseModesis specified, select the highest-priority mode that both client and server supportresponseModesis specified, default totaskmodeimmediatebut results are not immediately available, determine fallback:streamingif supportedtaskmodeStreaming Implementation:
Task Creation: Always create a task when using
streamingmodeSegment Generation: As results are produced:
seqNrvalues starting from 1seqNrvalues are unique and monotonically increasingTransmission Mechanisms:
partial-contentif segments are immediately availablepartial-contentin SSE eventslastSeqNrseqNrthrough multiple mechanismsCompletion Handling:
isComplete: truein the final streaming responsecompletedafter sending the final responseImplementation Checklist
Use this checklist to verify your implementation:
Server Implementation
tasks.responses.modesresponseModeslistCreateTaskResultfor task modeseqNrvalues starting from 1seqNrthrough multiple mechanismsisComplete: trueand transitions task tocompletedlastSeqNrintasks/resultrequestsClient Implementation
tasks.responses.modesresponseModesin task-augmented requestscontentfieldpartial-contentand tracks segmentsseqNrset to detect duplicatesseqNrseqNrvalues and requests them vialastSeqNrcontentarrayisCompletelastSeqNrto resume streamingTest Scenarios
Recommended test scenarios for validating implementations:
Basic Functionality
Immediate Mode - Complete Results:
immediatemodecontentfield with complete resultsTask Mode - Standard Flow:
taskmode (or no mode specified)CreateTaskResulttasks/resultcontentformatStreaming Mode - Initial Response Only:
streamingmodepartial-contentisComplete: truein initial responseStreaming Scenarios
Streaming Mode - Multiple Responses:
streamingmodepartial-content(seqNr 1-2)partial-content(seqNr 3-4)isComplete: trueStreaming Mode - SSE Stream:
streamingmode via Streamable HTTP transportpartial-contentStreaming Mode - Resume with lastSeqNr:
tasks/resultwithlastSeqNr: 3Streaming Mode - Gap Detection:
tasks/resultwithlastSeqNr: 1Edge Cases
Immediate Mode Fallback:
immediatemodetaskorstreamingmode appropriatelyTTL Expiration During Streaming:
Task Cancellation During Streaming:
tasks/cancelcancelledTask Failure During Streaming:
isError: trueandisComplete: trueDuplicate Segment Detection:
seqNrthrough multiple mechanismsCompatibility
New Client + Old Server:
streamingmodetaskmodeOld Client + New Server:
responseModestaskmodeMixed Mechanisms: