Skip to content
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

Add timeout reset on progress notifications #152

Conversation

Ozamatash
Copy link
Contributor

Add timeout reset on progress notifications

Closes #87

Motivation and Context

This change addresses the need for better handling of long-running operations in the MCP protocol. Previously, requests would time out according to a fixed timeout, even if the server was actively sending progress notifications. This could lead to premature timeouts for valid long-running operations.

The implementation allows servers to indicate that an operation is still in flight by sending progress notifications, which will reset the timeout. To prevent indefinite operations, a configurable maximum total timeout is also provided.

How Has This Been Tested?

  • Basic timeout reset on progress notification
  • Maximum total timeout enforcement
  • Multiple sequential progress notifications
  • Default timeout behavior without progress
  • Edge cases around timeout boundaries

Breaking Changes

No breaking changes. The implementation is fully backwards compatible:

  • New options (resetTimeoutOnProgress and maxTotalTimeout) are optional
  • Default timeout behavior remains unchanged
  • Existing progress notification handling is preserved

Types of changes

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

Checklist

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

Additional context

Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

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

Thanks for working on this! ✨ Left some comments below, but I'd love to get this merged after they're addressed.

@@ -62,6 +62,182 @@ describe("protocol tests", () => {
await transport.close();
expect(oncloseMock).toHaveBeenCalled();
});

test("should reset timeout when progress notification is received", async () => {
jest.useFakeTimers();
Copy link
Member

Choose a reason for hiding this comment

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

Can you please put this into a beforeEach and useRealTimers into an afterEach? (Consider using a describe to group with the below tests too.)

Right now, errors in the test could cause timers not to be restored.

progress: 50,
total: 100,
});

Copy link
Member

Choose a reason for hiding this comment

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

Presumably we need to advance the timers again here? Otherwise this doesn't seem to really be testing that the timeout got extended.

Comment on lines 141 to 155
// Advance time beyond maxTotalTimeout
jest.advanceTimersByTime(150);

// Send progress notification after maxTotalTimeout
if (transport.onmessage) {
transport.onmessage({
jsonrpc: "2.0",
method: "notifications/progress",
params: {
progressToken: 0,
progress: 50,
total: 100,
},
});
}
Copy link
Member

Choose a reason for hiding this comment

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

We should test that sending a progress notification doesn't reset the max total timeout, which AFAICT this doesn't exercise—because the timeout already happened by the time the progress notification is sent.


/**
* Maximum total time (in milliseconds) to wait for a response, even if progress notifications are received.
* Only used when resetTimeoutOnProgress is true.
Copy link
Member

Choose a reason for hiding this comment

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

It might be a bit confusing that this property only has an effect when another is set. Any reason not to always apply it?

Comment on lines 200 to 204
cancel(new McpError(
ErrorCode.RequestTimeout,
"Maximum total timeout exceeded",
{ maxTotalTimeout: info.maxTotalTimeout, totalElapsed }
));
Copy link
Member

Choose a reason for hiding this comment

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

TIOLI: I think the flow here is a bit confusing, and IMO it'd be cleaner to throw this as an exception here, rather than chain callbacks.

@Ozamatash
Copy link
Contributor Author

  1. Test improvements:

    • Moved timer setup/cleanup into beforeEach/afterEach hooks.
    • Added timer advancement after progress notification to properly test timeout extension
    • Fixed maxTotalTimeout test to verify progress notifications don't reset after timeout
  2. Code changes:

    • Made maxTotalTimeout always apply, independent of resetTimeoutOnProgress
    • Changed error handling to throw exception directly instead of callbacks.
    • Added try-catch in _onprogress to properly handle timeout errors

Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

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

Thanks, looking good! Just one remaining question—sorry for overlooking it the first time.

Comment on lines 363 to 365
if (!this._resetTimeout(messageId)) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

IIUC this means that not having a timeout handler will skip calling the progress handler. Is that intentional? (Sorry for missing it in the first review!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not intentional. Although we only enter the if block if timeoutInfo exists. Changed it to only return early on actual timeout errors.

Copy link
Member

@jspahrsummers jspahrsummers left a comment

Choose a reason for hiding this comment

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

Great, thanks!

@jspahrsummers jspahrsummers merged commit 5c07636 into modelcontextprotocol:main Feb 24, 2025
2 checks passed
@GLips
Copy link

GLips commented Feb 24, 2025

This looks interesting for an issue I'm working on. Are there updated docs for this feature? I can't tell directly from the PR how I might use it—I don't think I have direct access to the protocol.request function in my implementation.

@Ozamatash Ozamatash deleted the feature/progress-timeout-reset branch February 24, 2025 23:30
@Ozamatash
Copy link
Contributor Author

On the server side you need to be sending notification messages and on the client side this feature needs to be enabled in the RequestOptions. This commit wasn't included in the latest release so it's not yet in the official npm package if I understand correctly. Basically mcp clients like Cursor still need to enable this.

@GLips
Copy link

GLips commented Feb 27, 2025

Appreciate the response @Ozamatash—I'm not clear on how to send notification messages from looking at the typescript SDK documentation or the tests in your PR, but sounds like if I hold tight support (and docs) for this will percolate through the ecosystem. Thanks for the good work!

MediaInfluences pushed a commit to MediaInfluences/typescript-sdk that referenced this pull request Apr 3, 2025
…ogress-timeout-reset

Add timeout reset on progress notifications
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.

Request timeouts could automatically reset when progress notifications are received
3 participants