Skip to content
Open
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/fix-reset-timeout-without-onprogress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@modelcontextprotocol/client': patch
'@modelcontextprotocol/server': patch
---

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.
12 changes: 7 additions & 5 deletions packages/core-internal/src/shared/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,14 +1164,14 @@ export abstract class Protocol<ContextT extends BaseContext> {
const messageId = Number(progressToken);

const handler = this._progressHandlers.get(messageId);
if (!handler) {
const responseHandler = this._responseHandlers.get(messageId);
const timeoutInfo = this._timeoutInfo.get(messageId);

if (!handler && !responseHandler) {
this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`));
return;
}

const responseHandler = this._responseHandlers.get(messageId);
const timeoutInfo = this._timeoutInfo.get(messageId);

if (timeoutInfo && responseHandler && timeoutInfo.resetTimeoutOnProgress) {
try {
this._resetTimeout(messageId);
Expand All @@ -1185,7 +1185,7 @@ export abstract class Protocol<ContextT extends BaseContext> {
}
}

handler(params);
handler?.(params);
}

/**
Expand Down Expand Up @@ -1427,6 +1427,8 @@ export abstract class Protocol<ContextT extends BaseContext> {

if (options?.onprogress) {
this._progressHandlers.set(messageId, options.onprogress);
}
if (options?.onprogress || options?.resetTimeoutOnProgress) {
jsonrpcRequest.params = {
...request.params,
_meta: {
Expand Down
49 changes: 49 additions & 0 deletions packages/core-internal/test/shared/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,55 @@ describe('protocol tests', () => {
await expect(requestPromise).resolves.toEqual({ result: 'success' });
});

test('should reset timeout without an onprogress handler', async () => {
await protocol.connect(transport);
const request = { method: 'example', params: {} };
const mockSchema: ZodType<{ result: string }> = z.object({
result: z.string()
});
const onErrorMock = vi.fn();
protocol.onerror = onErrorMock;

const requestPromise = protocol.request(request, mockSchema, {
timeout: 1000,
resetTimeoutOnProgress: true
});

expect(sendSpy).toHaveBeenCalledWith(
expect.objectContaining({
params: {
_meta: {
progressToken: 0
}
}
}),
expect.any(Object)
);

vi.advanceTimersByTime(800);
transport.onmessage?.({
jsonrpc: '2.0',
method: 'notifications/progress',
params: {
progressToken: 0,
progress: 50,
total: 100
}
});
await Promise.resolve();

expect(onErrorMock).not.toHaveBeenCalled();

vi.advanceTimersByTime(800);
transport.onmessage?.({
jsonrpc: '2.0',
id: 0,
result: { result: 'success' }
});
await Promise.resolve();
await expect(requestPromise).resolves.toEqual({ result: 'success' });
});

test('should respect maxTotalTimeout', async () => {
await protocol.connect(transport);
const request = { method: 'example', params: {} };
Expand Down
Loading