Skip to content

Commit aa47d06

Browse files
committed
fix(core): reset timeout on progress without handler
1 parent cc4b416 commit aa47d06

3 files changed

Lines changed: 62 additions & 5 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
'@modelcontextprotocol/server': patch
4+
---
5+
6+
Fix `resetTimeoutOnProgress` so it works without an `onprogress` handler. Requests that opt into timeout resets now advertise a progress token, reset their timeout when progress arrives, and no longer report progress for the known in-flight request as an unknown-token error.

packages/core-internal/src/shared/protocol.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,14 +1164,14 @@ export abstract class Protocol<ContextT extends BaseContext> {
11641164
const messageId = Number(progressToken);
11651165

11661166
const handler = this._progressHandlers.get(messageId);
1167-
if (!handler) {
1167+
const responseHandler = this._responseHandlers.get(messageId);
1168+
const timeoutInfo = this._timeoutInfo.get(messageId);
1169+
1170+
if (!handler && !responseHandler) {
11681171
this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`));
11691172
return;
11701173
}
11711174

1172-
const responseHandler = this._responseHandlers.get(messageId);
1173-
const timeoutInfo = this._timeoutInfo.get(messageId);
1174-
11751175
if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) {
11761176
try {
11771177
this._resetTimeout(messageId);
@@ -1185,7 +1185,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
11851185
}
11861186
}
11871187

1188-
handler(params);
1188+
handler?.(params);
11891189
}
11901190

11911191
/**
@@ -1427,6 +1427,8 @@ export abstract class Protocol<ContextT extends BaseContext> {
14271427

14281428
if (options?.onprogress) {
14291429
this._progressHandlers.set(messageId, options.onprogress);
1430+
}
1431+
if (options?.onprogress || options?.resetTimeoutOnProgress) {
14301432
jsonrpcRequest.params = {
14311433
...request.params,
14321434
_meta: {

packages/core-internal/test/shared/protocol.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,55 @@ describe('protocol tests', () => {
444444
await expect(requestPromise).resolves.toEqual({ result: 'success' });
445445
});
446446

447+
test('should reset timeout without an onprogress handler', async () => {
448+
await protocol.connect(transport);
449+
const request = { method: 'example', params: {} };
450+
const mockSchema: ZodType<{ result: string }> = z.object({
451+
result: z.string()
452+
});
453+
const onErrorMock = vi.fn();
454+
protocol.onerror = onErrorMock;
455+
456+
const requestPromise = protocol.request(request, mockSchema, {
457+
timeout: 1000,
458+
resetTimeoutOnProgress: true
459+
});
460+
461+
expect(sendSpy).toHaveBeenCalledWith(
462+
expect.objectContaining({
463+
params: {
464+
_meta: {
465+
progressToken: 0
466+
}
467+
}
468+
}),
469+
expect.any(Object)
470+
);
471+
472+
vi.advanceTimersByTime(800);
473+
transport.onmessage?.({
474+
jsonrpc: '2.0',
475+
method: 'notifications/progress',
476+
params: {
477+
progressToken: 0,
478+
progress: 50,
479+
total: 100
480+
}
481+
});
482+
await Promise.resolve();
483+
484+
expect(onErrorMock).not.toHaveBeenCalled();
485+
486+
vi.advanceTimersByTime(800);
487+
transport.onmessage?.({
488+
jsonrpc: '2.0',
489+
id: 0,
490+
result: { result: 'success' }
491+
});
492+
await Promise.resolve();
493+
await expect(requestPromise).resolves.toEqual({ result: 'success' });
494+
});
495+
447496
test('should respect maxTotalTimeout', async () => {
448497
await protocol.connect(transport);
449498
const request = { method: 'example', params: {} };

0 commit comments

Comments
 (0)