-
Notifications
You must be signed in to change notification settings - Fork 493
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
Add timeout reset on progress notifications #152
Conversation
There was a problem hiding this 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.
src/shared/protocol.test.ts
Outdated
@@ -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(); |
There was a problem hiding this comment.
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.
src/shared/protocol.test.ts
Outdated
progress: 50, | ||
total: 100, | ||
}); | ||
|
There was a problem hiding this comment.
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.
src/shared/protocol.test.ts
Outdated
// 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, | ||
}, | ||
}); | ||
} |
There was a problem hiding this comment.
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.
src/shared/protocol.ts
Outdated
|
||
/** | ||
* Maximum total time (in milliseconds) to wait for a response, even if progress notifications are received. | ||
* Only used when resetTimeoutOnProgress is true. |
There was a problem hiding this comment.
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?
src/shared/protocol.ts
Outdated
cancel(new McpError( | ||
ErrorCode.RequestTimeout, | ||
"Maximum total timeout exceeded", | ||
{ maxTotalTimeout: info.maxTotalTimeout, totalElapsed } | ||
)); |
There was a problem hiding this comment.
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.
|
There was a problem hiding this 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.
src/shared/protocol.ts
Outdated
if (!this._resetTimeout(messageId)) { | ||
return; | ||
} |
There was a problem hiding this comment.
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!)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, thanks!
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 |
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. |
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! |
…ogress-timeout-reset Add timeout reset on progress notifications
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?
Breaking Changes
No breaking changes. The implementation is fully backwards compatible:
resetTimeoutOnProgress
andmaxTotalTimeout
) are optionalTypes of changes
Checklist
Additional context