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
331 changes: 330 additions & 1 deletion src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
ListRootsRequestSchema,
ErrorCode,
McpError,
CreateTaskResultSchema
CreateTaskResultSchema,
Tool,
Prompt,
Resource
} from '../types.js';
import { Transport } from '../shared/transport.js';
import { Server } from '../server/index.js';
Expand Down Expand Up @@ -1229,6 +1232,332 @@ test('should handle request timeout', async () => {
});
});

/***
* Test: Handle Tool List Changed Notifications with Auto Refresh
*/
test('should handle tool list changed notification with auto refresh', async () => {
// List changed notifications
const notifications: [Error | null, Tool[] | null][] = [];

const server = new McpServer({
name: 'test-server',
version: '1.0.0'
});

// Register initial tool to enable the tools capability
server.registerTool(
'initial-tool',
{
description: 'Initial tool'
},
async () => ({ content: [] })
);

// Configure listChanged handler in constructor
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
listChanged: {
tools: {
onChanged: (err, tools) => {
notifications.push([err, tools]);
}
}
}
}
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

const result1 = await client.listTools();
expect(result1.tools).toHaveLength(1);

// Register another tool - this triggers listChanged notification
server.registerTool(
'test-tool',
{
description: 'A test tool'
},
async () => ({ content: [] })
);

// Wait for the debounced notifications to be processed
await new Promise(resolve => setTimeout(resolve, 1000));

// Should be 1 notification with 2 tools because autoRefresh is true
expect(notifications).toHaveLength(1);
expect(notifications[0][0]).toBeNull();
expect(notifications[0][1]).toHaveLength(2);
expect(notifications[0][1]?.[1].name).toBe('test-tool');
});

/***
* Test: Handle Tool List Changed Notifications with Manual Refresh
*/
test('should handle tool list changed notification with manual refresh', async () => {
// List changed notifications
const notifications: [Error | null, Tool[] | null][] = [];

const server = new McpServer({
name: 'test-server',
version: '1.0.0'
});

// Register initial tool to enable the tools capability
server.registerTool('initial-tool', {}, async () => ({ content: [] }));

// Configure listChanged handler with manual refresh (autoRefresh: false)
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
listChanged: {
tools: {
autoRefresh: false,
debounceMs: 0,
onChanged: (err, tools) => {
notifications.push([err, tools]);
}
}
}
}
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

const result1 = await client.listTools();
expect(result1.tools).toHaveLength(1);

// Register another tool - this triggers listChanged notification
server.registerTool(
'test-tool',
{
description: 'A test tool'
},
async () => ({ content: [] })
);

// Wait for the notifications to be processed (no debounce)
await new Promise(resolve => setTimeout(resolve, 100));

// Should be 1 notification with no tool data because autoRefresh is false
expect(notifications).toHaveLength(1);
expect(notifications[0][0]).toBeNull();
expect(notifications[0][1]).toBeNull();
});

/***
* Test: Handle Prompt List Changed Notifications
*/
test('should handle prompt list changed notification with auto refresh', async () => {
const notifications: [Error | null, Prompt[] | null][] = [];

const server = new McpServer({
name: 'test-server',
version: '1.0.0'
});

// Register initial prompt to enable the prompts capability
server.registerPrompt(
'initial-prompt',
{
description: 'Initial prompt'
},
async () => ({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }]
})
);

// Configure listChanged handler in constructor
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
listChanged: {
prompts: {
onChanged: (err, prompts) => {
notifications.push([err, prompts]);
}
}
}
}
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

const result1 = await client.listPrompts();
expect(result1.prompts).toHaveLength(1);

// Register another prompt - this triggers listChanged notification
server.registerPrompt('test-prompt', { description: 'A test prompt' }, async () => ({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }]
}));

// Wait for the debounced notifications to be processed
await new Promise(resolve => setTimeout(resolve, 1000));

// Should be 1 notification with 2 prompts because autoRefresh is true
expect(notifications).toHaveLength(1);
expect(notifications[0][0]).toBeNull();
expect(notifications[0][1]).toHaveLength(2);
expect(notifications[0][1]?.[1].name).toBe('test-prompt');
});

/***
* Test: Handle Resource List Changed Notifications
*/
test('should handle resource list changed notification with auto refresh', async () => {
const notifications: [Error | null, Resource[] | null][] = [];

const server = new McpServer({
name: 'test-server',
version: '1.0.0'
});

// Register initial resource to enable the resources capability
server.registerResource('initial-resource', 'file:///initial.txt', {}, async () => ({
contents: [{ uri: 'file:///initial.txt', text: 'Hello' }]
}));

// Configure listChanged handler in constructor
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
listChanged: {
resources: {
onChanged: (err, resources) => {
notifications.push([err, resources]);
}
}
}
}
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

const result1 = await client.listResources();
expect(result1.resources).toHaveLength(1);

// Register another resource - this triggers listChanged notification
server.registerResource('test-resource', 'file:///test.txt', {}, async () => ({
contents: [{ uri: 'file:///test.txt', text: 'Hello' }]
}));

// Wait for the debounced notifications to be processed
await new Promise(resolve => setTimeout(resolve, 1000));

// Should be 1 notification with 2 resources because autoRefresh is true
expect(notifications).toHaveLength(1);
expect(notifications[0][0]).toBeNull();
expect(notifications[0][1]).toHaveLength(2);
expect(notifications[0][1]?.[1].name).toBe('test-resource');
});

/***
* Test: Handle Multiple List Changed Handlers
*/
test('should handle multiple list changed handlers configured together', async () => {
const toolNotifications: [Error | null, Tool[] | null][] = [];
const promptNotifications: [Error | null, Prompt[] | null][] = [];

const server = new McpServer({
name: 'test-server',
version: '1.0.0'
});

// Register initial tool and prompt to enable capabilities
server.registerTool(
'tool-1',
{
description: 'Tool 1'
},
async () => ({ content: [] })
);
server.registerPrompt(
'prompt-1',
{
description: 'Prompt 1'
},
async () => ({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }]
})
);

// Configure multiple listChanged handlers in constructor
const client = new Client(
{
name: 'test-client',
version: '1.0.0'
},
{
listChanged: {
tools: {
debounceMs: 0,
onChanged: (err, tools) => {
toolNotifications.push([err, tools]);
}
},
prompts: {
debounceMs: 0,
onChanged: (err, prompts) => {
promptNotifications.push([err, prompts]);
}
}
}
}
);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();

await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);

// Register another tool and prompt to trigger notifications
server.registerTool(
'tool-2',
{
description: 'Tool 2'
},
async () => ({ content: [] })
);
server.registerPrompt(
'prompt-2',
{
description: 'Prompt 2'
},
async () => ({
messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }]
})
);

// Wait for notifications to be processed
await new Promise(resolve => setTimeout(resolve, 100));

// Both handlers should have received their respective notifications
expect(toolNotifications).toHaveLength(1);
expect(toolNotifications[0][1]).toHaveLength(2);

expect(promptNotifications).toHaveLength(1);
expect(promptNotifications[0][1]).toHaveLength(2);
});

describe('outputSchema validation', () => {
/***
* Test: Validate structuredContent Against outputSchema
Expand Down
Loading
Loading