Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 276 additions & 0 deletions proposals/MRTR-And-Tasks-Message-Structure-Proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
# MRTR And Tasks Message Structure Proposal

## Overview
This document provides an overview of how the [Multi-Round-Trip Request Proposal](https://github.com/modelcontextprotocol/transports-wg/pull/7/changes#diff-c42674696a4c91ccc0d2daf8425dbcb52201ec1ef75921ae1e4865b5b911018d) (MRTR) fits with the Tasks by walking through an example with Elicitations.

A few notes the Goals of MRTR are to:
1. Ensure the protocol itself is stateless while allowing for stateful application semantics
2. Making the GET SSE stream truly optional, i.e. not required for core functionality
3. Remove the ability for sampling/elicitation to be sent out of band (i.e. without a client request).

In Tasks this means:
1. TaskId is used to represent the application state
2. Remove the guidance around using SSE stream, and model the call flow as request/response semantics only.

Tasks & Tool Calls provide mecahnisms for implementing two different kinds of messaging patterns.
1. Tool Calls: short running/sync/stateless - i.e. return within ms/seconds, low cost to compute/answer.
2. Tasks: long running/async/stateful - i.e. can run for minutes to hours, higher cost to compute. Server may be storing state between calls related to TaskId.

Given the above these patterns will manifest MRTR in two different ways.
1. Tool Calls: Server sends a Result with an elicitation/sampling request. The server stops processing the request at this point, and the client must retry
2. Tasks: Server sets status to input_required. The client will make a request for the Result which should contain the elicitation/sampling request. The server can pause processing, while the client gathers the info, and then updates the server. Once the necessary info has been retrieved the server can resume processing.

It also raises open questions and proposed solutions for discussion on what the return type should be for Tool Requests that require Elicitation or Sampling to complete.

## Tasks Background
The [Tasks](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks) Utilities allows requests to be augmented with a Promise like mechanism. `Tasks` have a Status Lifecycle including `Working`, `Input Requried`, `Completed`, `Failed`, and `Cancelled`.

The `Input Required` status allows a Tool or Capability to indicate that additional input is required from the user to complete the task. This is where an Elicitation or Sampling request can be made by the server.

When the client encounters an `Input Required` status it SHOULD call `tasks/result`. This allows the server to then return an `Elicitation` or `Sampling` request to the client. This fits well with the proposal to eliminate unsolicited `Elicitation` and `Sampling` requests from the server to the client.

### Example Flow with Elicitations
The below example uses an Echo Tool with an optional input parameter, when missing Elicitation is used to request the input from the user before completing the request.

1. <b>Client Request</b> to invoke EchoTool.
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "echo",
"task": {
"ttl": 60000
}
}
}
```

2. <b>Server Response</b> with a `Task`
```json
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"task": {
"taskId":"echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5",
"status": "Working",
"statusMessage": "Task has been created for echo tool invocation.",
"createdAt": "2026-01-27T03:32:48.3148180Z",
"ttl": 60000,
"pollInterval": 100
}
}
}
```

3. <b>Client Request</b> periodically checks the status of the `Task` using `tasks/get`.
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tasks/get",
"params": {
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
}
}
```

4. <b>Server Response</b> with Task status `InputRequired`
```json
{
"id": 2,
"jsonrpc": "2.0",
"result":
{
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5",
"status": "input_required",
"statusMessage": "Input Required to Proceed call tasks/result",
"createdAt": "2026-01-27T03:38:07.7534643Z",
"ttl": 60000,
"pollInterval": 100
},
}
```

5. <b>Client Request</b> sends message `tasks/result` to discover what input is required to proceed.
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/result",
"params": {
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
}
}
```

6. <b>Server Response</b> returns `elicitation/create` to request additional input
```json
{
"id": 3,
Comment on lines +109 to +112
Copy link
Copy Markdown

@LucaButBoring LucaButBoring Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't quite right as stated - this is:

  • a server-to-client request (with its own request ID)
  • that occurs as a side-effect of a tasks/result client-to-server request

In Tasks, we're not altering the fundamental request/response flow (I mean we are actually with CreateTaskResult, but that's it) - what we're doing is creating an opportunity for the server to open an SSE stream to side-channel an elicitation request on, independent of the final task result itself. The server is actually allowed to open this side-channel in response to any request (or even send messages on the background stream), but we specifically require tasks/result to block until a terminal state to reserve it for this purpose.

The sequence diagram from the spec shows the client terminating the stream after receiving the elicitation request, but that's not actually required even though it is allowed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sequence diagram from the spec shows the client terminating the stream after receiving the elicitation request, but that's not actually required even though it is allowed.

Does the TS SDK client terminate the result stream after receiving a request? I assume not. If it did, it would by heavily relying on Last-Event-Id based stream resumability to work so it doesn't miss any messages when it comes back, and I'm not sure how widely supported that is even for MCP servers that support tasks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LucaButBoring ack on how it behaves today. One of the goals with the MRTR is to eliminate Server initiated messages, so this is the proposal on how the request flow would change to accommodate that today.

Also in the December meetup we discussed having the SSE be an option for performance but not a required part of functionality since it is Optional in the protocol itself. Additionally the current behavior adds a lot of complexity for what could be a request/response pattern.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sequence diagram from the spec shows the client terminating the stream after receiving the elicitation request, but that's not actually required even though it is allowed.

Does the TS SDK client terminate the result stream after receiving a request? I assume not. If it did, it would by heavily relying on Last-Event-Id based stream resumability to work so it doesn't miss any messages when it comes back, and I'm not sure how widely supported that is even for MCP servers that support tasks.

The client does not proactively terminate streams, correct - that flow can use regular stream resumability there, too (though with MRTR we might be able to do away with even that for the most-common use cases).

"jsonrpc": "2.0",
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please provide the input string to echo back",
"requestedSchema":
{
"type": "object",
"properties":
{
"input": {"type": "string"}
},
"required": ["input"]
}
},
"_meta":
{
"io.modelcontextprotocol/related-task":
{
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
}
}
}
```

7. <b>Client Request</b> presents the Elicitation to the user and collects the input, then sends message to the server.
```json
{
"jsonrpc": "2.0",
"id": 4,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should reflect the request ID of the elicitation request

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 That probably means that the elicitation/create request should get a new ID rather than reusing the tasks/result request ID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call outs, I think this is a place where the spec is ambiguous and we should align ourselves here. Right now we have MCP-Session-Id (slated to be removed), MessageId, and TaskId all encapsulating some state.

Is the JsonRPC Envelope Id a piece of state that persists across sessions, or is it something that is tied to request/response flow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the JsonRPC Envelope Id a piece of state that persists across sessions, or is it something that is tied to request/response flow.

From requests:

The request ID MUST NOT have been previously used by the requestor within the same session.

But notably, this part of the spec doesn't actually define what a session is. I am at least certain that multiple clients can use the same request IDs as each other with a given server.

"result": {
"action": "accept",
"content": {
"input": "Hello World!"
},
"_meta": {
"io.modelcontextprotocol/related-task": {
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
}
}
}
}
```

8. <b>Server Response</b> .Currently there is no required response to this message, but the server can now proceed to complete the `Task` using the provided input, and the `Task` status changes to `Working`

9. <b>Client Request</b> continues to poll the input status using `tasks/get` until server responds with Task Status of `Completed`
Client Request
```json
{
"jsonrpc": "2.0",
"id": 5,
"method": "tasks/get",
"params": {
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
}
}
```
10. <b>Server Response</b> with Task status `Completed`
```json
{
"id": 5,
"jsonrpc": "2.0",
"result":
{
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5",
"status": "Completed",
"statusMessage": "Task has been completed successfully, call get/result",
"createdAt": "2026-01-27T03:38:07.7534643Z",
"ttl": 60000,
"pollInterval": 100
},
}
```

11. <b>Client Request</b> calls `tasks/result` to get the final result of the `Task` from the server.
Client Message
```json
{
"id": 6,
Comment on lines +188 to +192
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client is also allowed to keep a stream from the initial tasks/result request open, in which case the server response will have an id of 3 (as that would be the response to the tasks/result request).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha in how it works today, see above I'm trying to model how this would work without requiring the SSE stream since its marked as Optional in the spec.

"jsonrpc": "2.0",
"method": "tasks/result",
"params":
{
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
},
}
```
12. <b>Server Response</b> with the final result of the `Task`
```json
{
"id": 6,
"jsonrpc": "2.0",
"result":
{
"isError": false,
"content":
[{
"type": "text",
"text": "Echo: Hello World!"
}],
"_meta":
{
"io.modelcontextprotocol/related-task":
{
"taskId": "echo_dc792e24-01b5-4c0a-abcb-0559848ca3c5"
}
}
},
}
```

## Discussion Points
Both Tool Calls and Task Results should follow the same pattern when requesting additional input via Elicitation or Sampling. Having different mechanisms and messaging pattern leads to complexity in implementation and confusion.

In both implementations a request for more information is treated as a special result. This can be viewed as a recoverable error case. In Tasks the request for more input is retrieved via the `tasks/result` message, while in Tool Calls it is returned directly as the result of the `tools/call` message.

Given the below options for response types should be considered.

### Option One - MRTR & Tasks return existing Elicitaiton or Sampling Messagees.
Today this is what Tasks does. The response to a `tasks/result` call when additional input is required is to return an `elicitation/create` or `sampling/createMessage` message.

<b>Pros:</b> Smaller changes to existing implementations.

<b>Cons:</b> Message structure does not align with the `result` message structure used by Completed Results and Error Messages.

### Option Two - MRTR & Tasks return a Result Wrapper around Elicitation & Sampling
This would involve defining a new [`ToolResult`](https://modelcontextprotocol.io/specification/draft/server/tools#tool-result) Content type that wraps an Elicitation or Sampling request.

This could look like, and would replace the response in step 6 above:
```json
{
"id": 3,
"jsonrpc": "2.0",
"result":{
"content": [
{
"type": "elicitation",
"mode": "form",
"message": "Please provide the input string to echo back",
"requestedSchema":
{
"type": "object",
"properties":
{
"input": {"type": "string"}
},
"required": ["input"]
}
}],
"isError": false,
}
}
```
Comment on lines +243 to +266
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the client send the response back to the server, here?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume as part of the "dependent_responses" as described in #7 for MRTR or continue as part of an independent request for Tasks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that Elicitation responses get sent to the POST /mcp endpoint today anyway. It would include the task metadata to be routed to the right task on the server handling the request. Because we have the task_id as the unifying session/state identifier for this request we can use that to route, and don't need a persistent connection, MCP-Session-Id or the Json-RPC Id.

This is how I have it in my prototype implementation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more wondering how separate elicitation requests are kept unique - today that's via request IDs, and in the MRTR example in #7, that's via named identifiers, but there doesn't seem to be anything equivalent here. A task can have multiple elicitations, so the task ID alone isn't enough to disambiguate them.


Pros:
- Consistent message structure for all Result types from Tasks & Tools which simplifies the SDK implementations.
- Consistent handling of isError or other future result metadata fields.
- With Tasks this structure would allow for partial results & additional input to requested on the same get/results
- Supports multiple Elicitation & Sampling requests at the same time.

Cons: Larger change to existing implementations.
- Requires deprecating the existing `Elicitation` and `Sampling` messages returned by Tasks since out of band messages are no longer needed.
- Need to `ToolResult` Content schema as a `Utilities` `Results` schema to indicate it's not just for `Tools` since they are already being used by `Tasks` and will now be extended further.