Skip to content

[feat] Introduce partial results as part of progress notifications #383

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

siwachabhi
Copy link

@siwachabhi siwachabhi commented Apr 22, 2025

Added support for streaming partial results through progress notifications, allowing for incremental updates during long-running operations. This enables more responsive agent-based workflows where intermediate results can be shared while a top-level operation is still running. #111, #117, #314.

Motivation and Context

This enhancement addresses a key need identified in discussion #111 for "structured, formatted intermediate updates from server -> client, so a deep agent graph can provide information to the user even while a top-level tool is still being run."

Progress notifications were an ideal mechanism to extend for this purpose, as they already provide a communication channel during long-running operations. By adding support for partial results, we enable servers to stream incremental data to clients without waiting for operations to complete, creating a more responsive and interactive experience.

LSP's partial result progress has been referred for this implementation: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults, specially the recommendation around if server send partial results over progressNotification then final result should be empty.

How Has This Been Tested?

The implementation has been validated through:

  1. Schema consistency checks
  2. Documentation review
  3. Verification of compatibility with existing progress notification behavior
  4. The design follows existing MCP patterns, particularly using a JSON-based data structure that mirrors many other parts of the protocol. The implementation is intentionally flexible to support different types of partial results while maintaining a consistent structure.

Breaking Changes

None. This is a non-breaking change that adds new optional fields while maintaining backward compatibility with existing implementations. Clients that don't support partial results can simply ignore the new fields.

Types of changes

[x] New feature (non-breaking change which adds functionality)
[ ] Bug fix (non-breaking change which fixes an issue)
[ ] Breaking change (fix or feature that would cause existing functionality to change)
[x] Documentation update

Checklist

[x] I have read the MCP Documentation
[x] My code follows the repository's style guidelines
[x] New and existing tests pass locally
[x] I have added appropriate error handling
[x] I have added or updated documentation as needed

Additional context

The implementation adds three key components:

  1. Client Request Flag: A new partialResults boolean in the request _meta object allows clients to explicitly request streaming results.
  2. Partial Result Structure: A new partialResult field in the ProgressNotification interface with:
    a. chunk: The partial result data (any JSON object)
    b. append: Boolean flag indicating whether to append to previously received chunks
    c. lastChunk: Boolean flag indicating the final chunk

Protocol

To request partial results, the client includes both a progressToken and partialResults: true in the request metadata:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "_meta": {
      "progressToken": "abc123",
      "partialResults": true
    },
    "name": "analyzeData",
    "arguments": {
      "datasetId": "financial-q1-2025"
    }
  }
}

The server can then include partial results in progress notifications:

{
  "jsonrpc": "2.0",
  "method": "notifications/progress",
  "params": {
    "progressToken": "abc123",
    "progress": 30,
    "total": 100,
    "message": "Initial analysis complete",
    "partialResult": {
      "chunk": {
        "content": [
          {
            "type": "text",
            "text": "## Preliminary Analysis\n\nData processing has revealed initial patterns..."
          }
        ]
      },
      "append": false,
      "lastChunk": false
    }
  }
}
sequenceDiagram
    participant Sender
    participant Receiver

    Note over Sender,Receiver: Request with progress token and partialResults
    Sender->>Receiver: Method request with progressToken and partialResults=true

    Note over Sender,Receiver: Progress updates with partial results
    Receiver-->>Sender: Progress notification (30%, initial chunk)
    Receiver-->>Sender: Progress notification (60%, append=true)
    Receiver-->>Sender: Progress notification (100%, lastChunk=true)

    Note over Sender,Receiver: Operation complete
    Receiver->>Sender: Empty result response
Loading

Comment on lines +1323 to +1327
"chunk": {
"additionalProperties": {},
"description": "The partial result data chunk.",
"type": "object"
},

Choose a reason for hiding this comment

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

question:

  1. there is nothing technically wrong with modeling this as a dict[str, Any], but this is not how CallToolResult models its content. Just curious if there is a specific reason not to keep same structure as CallToolResult? Intuitively i would expect them to be very similar.
  2. Also a nitpick on naming but should we call this chunk? why not "content" or something (similar to CallToolResult)

Copy link
Author

Choose a reason for hiding this comment

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

1/ It was from perspective that progress is not tool specific, even server to client request can have a progress
2/ Yeah also totally good option, only reason is chunk intuitively gives a sense of something incomplete.

Copy link

@000-000-000-000-000 000-000-000-000-000 May 9, 2025

Choose a reason for hiding this comment

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

  1. I dont think the objects in content of CallToolResult are like tied to CallTool - they are generic content types. They're useful for consumer to be able to write logic against the various response modalities. If we return arbitrary JSON then we almost need a new schema for this. Also you could include DataContent from this PR: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/356/files to achieve the JSON part too.
  2. IMO chunk is kind of specific to LLM nomenclature. I would suggest something more generic either borrowing form CallToolResult "content" or something like "partialResult"

Copy link
Author

Choose a reason for hiding this comment

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

  1. Whole RPC result object can be in chunk/content, each result can have different structure. So intention of modeling it like dict is to model it like https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-03-26/schema.ts#L61
    a. Headsup I think that PR got abandoned and RFC: add Tool.outputSchema and CallToolResult.structuredContent #371 is merged.
  2. Notice key just outside of this one is also called partialResult. But not tied to chunk, content is also fine.

@@ -844,6 +844,10 @@
"properties": {
"_meta": {
"properties": {
"partialResults": {

Choose a reason for hiding this comment

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

NIT: I feel that renaming this streamPartialResults or notifyPartialResults would be more apt than adding a boolean value under partialResults.

Copy link
Author

Choose a reason for hiding this comment

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

yes that makes sense, I can update

@LucaButBoring
Copy link

LucaButBoring commented May 16, 2025

How should this be handled by clients when not all results are delivered, or they aren't all received in the order they were sent? As I've noted in #430, notifications aren't guaranteed to be received in any particular order, as there's no way for the client to acknowledge receipt (also noted in the JSON-RPC spec).

@adamkaplan
Copy link

How should this be handled by clients when not all results are delivered, or they aren't all received in the order they were sent? As I've noted in #430, notifications aren't guaranteed to be received in any particular order, as there's no way for the client to acknowledge receipt (also noted in the JSON-RPC spec).

The progress number in the message increments monotonically, making it possible to recognize out of order delivery. However progress doesn’t need to have a predictable step, so recognizing missed result chunks is a problem.

It’s probably best to introduce a partial result ID that must increment monotonically by 1 when present. If out of order chunk is received, the client spec could be to disconnect and reconnect with resumption token. Unidirectional SSE is constraining in this regard… however even sync l task results don’t have a delivery receipt guarantee.

@LucaButBoring
Copy link

It’s probably best to introduce a partial result ID that must increment monotonically by 1 when present. If out of order chunk is received, the client spec could be to disconnect and reconnect with resumption token.

Would reconnection be sufficient here? The messages would have already been sent successfully, so the server would have no reason to buffer them anymore. It'd make sense for the client to hold an ordered buffer and reorder messages correctly according to the server-provided ID instead of forcing in-order receipt, and use a "task complete" notification with the final ID as a cutoff.

However, that still wouldn't handle the case where receipt doesn't happen at all, though. Not having any form of client acknowledgement is a fundamental flaw if partial results are intended to work as an alternative to receiving a response all at once. It'd work if partial results were represented with server->client requests instead of notifications, but that then introduces much more synchronization into the process, since the server needs to wait for an ack before sending the next part (it's also unintuitive, but setting that aside).


I almost wonder if it'd be better to do this by implementing something like ranges as an optional parameter on resources, and use progress as a way of telling the client where it can read from 🤔 That way this can be best-effort and tolerant of dropped messages (e.g. client holds its last successful read position, reads to the server-provided latest byte on progress notifications - if the operation completes, just read to the end).

@jonathanhefner
Copy link

I might have missed it, but why make partial results part of progress notifications? Why not have separate notifications for each?

One feature that I would like to see is mergeable partial results. For example, if you have a partial result with content: [a, b] and a subsequent partial result with content: [c], you can merge them into a partial result with content: [a, b, c]. I think that would be more difficult to achieve if partial results are entangled in progress notifications.

append: Boolean flag indicating whether to append to previously received chunks

Do we think append: true should be the default? If so, I would rename this to replace, such that replace: false becomes the default.

lastChunk: Boolean flag indicating the final chunk

Since the server will send an empty JSON-RPC response to cap things off, is this flag necessary?

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.

6 participants