Skip to content

Commit 327817c

Browse files
committed
fix(client): paginate prompt and resource list refresh
1 parent 06f9982 commit 327817c

2 files changed

Lines changed: 118 additions & 4 deletions

File tree

packages/client/src/client/client.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ import type {
2424
MessageExtraInfo,
2525
NotificationMethod,
2626
ProtocolOptions,
27+
Prompt,
2728
ReadResourceRequest,
2829
RequestMethod,
2930
RequestOptions,
31+
Resource,
3032
Result,
3133
ServerCapabilities,
3234
SubscribeRequest,
@@ -270,15 +272,13 @@ export class Client extends Protocol<ClientContext> {
270272

271273
if (config.prompts && this._serverCapabilities?.prompts?.listChanged) {
272274
this._setupListChangedHandler('prompts', 'notifications/prompts/list_changed', config.prompts, async () => {
273-
const result = await this.listPrompts();
274-
return result.prompts;
275+
return this._listAllPrompts();
275276
});
276277
}
277278

278279
if (config.resources && this._serverCapabilities?.resources?.listChanged) {
279280
this._setupListChangedHandler('resources', 'notifications/resources/list_changed', config.resources, async () => {
280-
const result = await this.listResources();
281-
return result.resources;
281+
return this._listAllResources();
282282
});
283283
}
284284
}
@@ -296,6 +296,32 @@ export class Client extends Protocol<ClientContext> {
296296
return tools;
297297
}
298298

299+
private async _listAllPrompts(): Promise<Prompt[]> {
300+
const prompts: Prompt[] = [];
301+
let cursor: string | undefined;
302+
303+
do {
304+
const result = await this.listPrompts(cursor === undefined ? undefined : { cursor });
305+
prompts.push(...result.prompts);
306+
cursor = result.nextCursor;
307+
} while (cursor !== undefined);
308+
309+
return prompts;
310+
}
311+
312+
private async _listAllResources(): Promise<Resource[]> {
313+
const resources: Resource[] = [];
314+
let cursor: string | undefined;
315+
316+
do {
317+
const result = await this.listResources(cursor === undefined ? undefined : { cursor });
318+
resources.push(...result.resources);
319+
cursor = result.nextCursor;
320+
} while (cursor !== undefined);
321+
322+
return resources;
323+
}
324+
299325
/**
300326
* Registers new capabilities. This can only be called before connecting to a transport.
301327
*

test/integration/test/client/client.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,94 @@ test('should handle resource list changed notification with auto refresh', async
14111411
expect(notifications[0]![1]?.[1]!.name).toBe('test-resource');
14121412
});
14131413

1414+
test('should auto-refresh all paginated prompts after prompt list changed notification', async () => {
1415+
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { prompts: { listChanged: true } } });
1416+
const listRequests: Array<string | undefined> = [];
1417+
const notifications: [Error | null, Prompt[] | null][] = [];
1418+
let resolveNotification: () => void = () => {};
1419+
const notification = new Promise<void>(resolve => {
1420+
resolveNotification = resolve;
1421+
});
1422+
1423+
server.setRequestHandler('prompts/list', async request => {
1424+
listRequests.push(request.params?.cursor);
1425+
if (request.params?.cursor === 'page-2') {
1426+
return { prompts: [{ name: 'prompt-2', description: 'second page' }] };
1427+
}
1428+
return { prompts: [{ name: 'prompt-1', description: 'first page' }], nextCursor: 'page-2' };
1429+
});
1430+
1431+
const client = new Client(
1432+
{ name: 'test-client', version: '1.0.0' },
1433+
{
1434+
listChanged: {
1435+
prompts: {
1436+
debounceMs: 0,
1437+
onChanged: (error, prompts) => {
1438+
notifications.push([error, prompts]);
1439+
resolveNotification();
1440+
}
1441+
}
1442+
}
1443+
}
1444+
);
1445+
1446+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1447+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
1448+
1449+
await server.notification({ method: 'notifications/prompts/list_changed' });
1450+
await notification;
1451+
1452+
expect(listRequests).toEqual([undefined, 'page-2']);
1453+
expect(notifications).toHaveLength(1);
1454+
expect(notifications[0]![0]).toBeNull();
1455+
expect(notifications[0]![1]?.map(prompt => prompt.name)).toEqual(['prompt-1', 'prompt-2']);
1456+
});
1457+
1458+
test('should auto-refresh all paginated resources after resource list changed notification', async () => {
1459+
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { resources: { listChanged: true } } });
1460+
const listRequests: Array<string | undefined> = [];
1461+
const notifications: [Error | null, Resource[] | null][] = [];
1462+
let resolveNotification: () => void = () => {};
1463+
const notification = new Promise<void>(resolve => {
1464+
resolveNotification = resolve;
1465+
});
1466+
1467+
server.setRequestHandler('resources/list', async request => {
1468+
listRequests.push(request.params?.cursor);
1469+
if (request.params?.cursor === 'page-2') {
1470+
return { resources: [{ name: 'resource-2', uri: 'file:///resource-2.txt' }] };
1471+
}
1472+
return { resources: [{ name: 'resource-1', uri: 'file:///resource-1.txt' }], nextCursor: 'page-2' };
1473+
});
1474+
1475+
const client = new Client(
1476+
{ name: 'test-client', version: '1.0.0' },
1477+
{
1478+
listChanged: {
1479+
resources: {
1480+
debounceMs: 0,
1481+
onChanged: (error, resources) => {
1482+
notifications.push([error, resources]);
1483+
resolveNotification();
1484+
}
1485+
}
1486+
}
1487+
}
1488+
);
1489+
1490+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
1491+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
1492+
1493+
await server.notification({ method: 'notifications/resources/list_changed' });
1494+
await notification;
1495+
1496+
expect(listRequests).toEqual([undefined, 'page-2']);
1497+
expect(notifications).toHaveLength(1);
1498+
expect(notifications[0]![0]).toBeNull();
1499+
expect(notifications[0]![1]?.map(resource => resource.name)).toEqual(['resource-1', 'resource-2']);
1500+
});
1501+
14141502
/***
14151503
* Test: Handle Multiple List Changed Handlers
14161504
*/

0 commit comments

Comments
 (0)