server-states.test.ts•7.84 kB
import { test, expect, describe, beforeAll, afterAll } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { writeFile, mkdir } from 'node:fs/promises';
const testConfigDir = 'tests/config-server-states';
const testConfigPath = `${testConfigDir}/settings.json`;
describe('Server States Test', () => {
let client: Client;
let transport: StdioClientTransport;
beforeAll(async () => {
// Create a test config with one autoConnect and one manual server
const testConfig = {
version: '1.0',
servers: {
filesystem: {
transport: 'stdio',
command: 'npx',
args: ['@modelcontextprotocol/server-filesystem'],
env: {},
autoConnect: true,
timeout: 30000,
},
'filesystem-manual': {
transport: 'stdio',
command: 'npx',
args: ['@modelcontextprotocol/server-filesystem'],
env: {},
autoConnect: false,
timeout: 30000,
},
},
logging: {
level: 'error',
},
};
// Create directory and write config
await mkdir(testConfigDir, { recursive: true });
await writeFile(testConfigPath, JSON.stringify(testConfig, null, 2));
// Create client with custom config
transport = new StdioClientTransport({
command: 'bun',
args: ['index.ts'],
env: {
...process.env,
MCP_CONFIG_DIR: testConfigDir,
},
});
client = new Client(
{
name: 'server-states-test-client',
version: '1.0.0',
},
{
capabilities: {
roots: { listChanged: true },
},
}
);
client.setRequestHandler(ListRootsRequestSchema, async () => ({
roots: [
{
uri: `file://${process.cwd()}`,
name: 'Test Directory',
},
],
}));
await client.connect(transport);
await waitForToolRegistration(client);
});
afterAll(async () => {
await client.close();
});
test('help tool should error for disabled server', async () => {
// This must be the first test to ensure filesystem hasn't been enabled yet
const result = await client.callTool({
name: 'help',
arguments: { server: 'filesystem' },
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
expect(text).toContain('not enabled');
expect(text).toContain('Use enable_server');
});
test('list_servers should show all servers with their states', async () => {
const result = await client.callTool({
name: 'list_servers',
arguments: {},
});
const content = result.content as Array<{ type: string; text: string }>;
const data = JSON.parse(content[0]?.text || '{}');
expect(data.servers).toBeDefined();
expect(data.servers.filesystem).toBeDefined();
expect(data.servers['filesystem-manual']).toBeDefined();
// filesystem should be connected (autoConnect: true)
expect(data.servers.filesystem.state).toBe('connected');
expect(data.servers.filesystem.toolCount).toBe(0); // No tools available when not enabled
// filesystem-manual should be not-connected (autoConnect: false)
expect(data.servers['filesystem-manual'].state).toBe('not-connected');
});
test('should not be able to call tools on connected but not enabled server', async () => {
const result = await client.callTool({
name: 'invoke',
arguments: {
calls: [
{
server: 'filesystem',
tool: 'list_directory',
parameters: { path: '.' },
},
],
},
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
// Should get an error about server not being enabled
expect(text).toContain('not enabled');
expect(text).toContain('state: connected');
});
test('enable_server should enable a connected server', async () => {
const result = await client.callTool({
name: 'enable_server',
arguments: { name: 'filesystem' },
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
expect(text).toContain("Server 'filesystem' enabled");
expect(text).toContain('tools available');
expect(text).toContain('TypeScript Declarations');
});
test('should be able to call tools after enabling server', async () => {
const result = await client.callTool({
name: 'eval',
arguments: {
function: 'async () => { const files = await filesystem.listDirectory({ path: "." }); return files.length > 0; }',
},
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
expect(text).toContain('$results');
expect(text).toContain('true');
});
test('list_servers should show enabled state after enabling', async () => {
const result = await client.callTool({
name: 'list_servers',
arguments: {},
});
const content = result.content as Array<{ type: string; text: string }>;
const data = JSON.parse(content[0]?.text || '{}');
expect(data.servers.filesystem.state).toBe('enabled');
expect(data.servers.filesystem.toolCount).toBeGreaterThan(0);
});
test('enable_server should connect and enable a not-connected server', async () => {
const result = await client.callTool({
name: 'enable_server',
arguments: { name: 'filesystem-manual' },
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
expect(text).toContain("Server 'filesystem-manual' enabled");
expect(text).toContain('tools available');
});
test('list_servers should show manual server as enabled after enable_server', async () => {
const result = await client.callTool({
name: 'list_servers',
arguments: {},
});
const content = result.content as Array<{ type: string; text: string }>;
const data = JSON.parse(content[0]?.text || '{}');
expect(data.servers['filesystem-manual'].state).toBe('enabled');
expect(data.servers['filesystem-manual'].toolCount).toBeGreaterThan(0);
});
test('enable_server should return message if already enabled', async () => {
const result = await client.callTool({
name: 'enable_server',
arguments: { name: 'filesystem' },
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
expect(text).toContain('already enabled');
});
test('enable_server should error for non-existent server', async () => {
const result = await client.callTool({
name: 'enable_server',
arguments: { name: 'nonexistent' },
});
const content = result.content as Array<{ type: string; text: string }>;
const text = content[0]?.text || '';
expect(text).toContain('not found');
expect(text).toContain('Available servers');
});
});
async function waitForToolRegistration(
client: Client,
maxRetries = 50,
delayMs = 100
): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
const result = await client.listTools();
const evalTool = result.tools?.find((tool) => tool.name === 'eval');
if (evalTool) {
return;
}
} catch (error) {
// Ignore errors and keep retrying
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error('Timeout waiting for tool registration to complete');
}