tasq/node_modules/@claude-flow/mcp/__tests__/mcp.test.ts

642 lines
19 KiB
TypeScript

/**
* @claude-flow/mcp - Test Suite
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
createMCPServer,
createToolRegistry,
createSessionManager,
createConnectionPool,
createResourceRegistry,
createPromptRegistry,
createTaskManager,
defineTool,
definePrompt,
textMessage,
createTextResource,
interpolate,
ErrorCodes,
MCPServerError,
VERSION,
MODULE_NAME,
} from '../src/index.js';
import type { ILogger, MCPTool } from '../src/types.js';
// Mock logger
const createMockLogger = (): ILogger => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
});
describe('@claude-flow/mcp', () => {
describe('Module exports', () => {
it('should export VERSION', () => {
expect(VERSION).toBe('3.0.0');
});
it('should export MODULE_NAME', () => {
expect(MODULE_NAME).toBe('@claude-flow/mcp');
});
it('should export ErrorCodes', () => {
expect(ErrorCodes.PARSE_ERROR).toBe(-32700);
expect(ErrorCodes.INVALID_REQUEST).toBe(-32600);
expect(ErrorCodes.METHOD_NOT_FOUND).toBe(-32601);
expect(ErrorCodes.INTERNAL_ERROR).toBe(-32603);
});
});
describe('MCPServerError', () => {
it('should create error with code', () => {
const error = new MCPServerError('Test error', ErrorCodes.INVALID_REQUEST);
expect(error.message).toBe('Test error');
expect(error.code).toBe(ErrorCodes.INVALID_REQUEST);
expect(error.name).toBe('MCPServerError');
});
it('should convert to MCP error format', () => {
const error = new MCPServerError('Test error', ErrorCodes.PARSE_ERROR, { extra: 'data' });
const mcpError = error.toMCPError();
expect(mcpError.code).toBe(ErrorCodes.PARSE_ERROR);
expect(mcpError.message).toBe('Test error');
expect(mcpError.data).toEqual({ extra: 'data' });
});
});
describe('ToolRegistry', () => {
let registry: ReturnType<typeof createToolRegistry>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
registry = createToolRegistry(logger);
});
it('should register a tool', () => {
const tool: MCPTool = {
name: 'test-tool',
description: 'A test tool',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ result: 'success' }),
};
const result = registry.register(tool);
expect(result).toBe(true);
expect(registry.hasTool('test-tool')).toBe(true);
expect(registry.getToolCount()).toBe(1);
});
it('should not register duplicate tools', () => {
const tool: MCPTool = {
name: 'test-tool',
description: 'A test tool',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ result: 'success' }),
};
registry.register(tool);
const result = registry.register(tool);
expect(result).toBe(false);
expect(registry.getToolCount()).toBe(1);
});
it('should override tool with option', () => {
const tool1: MCPTool = {
name: 'test-tool',
description: 'First version',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ result: 'v1' }),
};
const tool2: MCPTool = {
name: 'test-tool',
description: 'Second version',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ result: 'v2' }),
};
registry.register(tool1);
const result = registry.register(tool2, { override: true });
expect(result).toBe(true);
expect(registry.getTool('test-tool')?.description).toBe('Second version');
});
it('should unregister a tool', () => {
const tool: MCPTool = {
name: 'test-tool',
description: 'A test tool',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ result: 'success' }),
};
registry.register(tool);
const result = registry.unregister('test-tool');
expect(result).toBe(true);
expect(registry.hasTool('test-tool')).toBe(false);
});
it('should execute a tool', async () => {
const tool: MCPTool = {
name: 'test-tool',
description: 'A test tool',
inputSchema: { type: 'object', properties: {} },
handler: async (input: unknown) => ({ received: input }),
};
registry.register(tool);
const result = await registry.execute('test-tool', { test: 'data' });
expect(result.isError).toBe(false);
expect(result.content[0].type).toBe('text');
});
it('should return error for unknown tool', async () => {
const result = await registry.execute('unknown-tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Tool not found');
});
it('should filter by category', () => {
registry.register({
name: 'tool-a',
description: 'Tool A',
inputSchema: { type: 'object' },
handler: async () => ({}),
category: 'category-1',
});
registry.register({
name: 'tool-b',
description: 'Tool B',
inputSchema: { type: 'object' },
handler: async () => ({}),
category: 'category-2',
});
const tools = registry.getByCategory('category-1');
expect(tools.length).toBe(1);
expect(tools[0].name).toBe('tool-a');
});
it('should get stats', () => {
registry.register({
name: 'tool-a',
description: 'Tool A',
inputSchema: { type: 'object' },
handler: async () => ({}),
category: 'cat',
tags: ['tag1', 'tag2'],
});
const stats = registry.getStats();
expect(stats.totalTools).toBe(1);
expect(stats.totalCategories).toBe(1);
expect(stats.totalTags).toBe(2);
});
});
describe('defineTool', () => {
it('should create a tool definition', () => {
const tool = defineTool(
'my-tool',
'My tool description',
{ type: 'object', properties: { input: { type: 'string' } } },
async (input) => ({ result: input }),
{ category: 'test', tags: ['tag1'] }
);
expect(tool.name).toBe('my-tool');
expect(tool.description).toBe('My tool description');
expect(tool.category).toBe('test');
expect(tool.tags).toEqual(['tag1']);
});
});
describe('SessionManager', () => {
let manager: ReturnType<typeof createSessionManager>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
manager = createSessionManager(logger, { sessionTimeout: 1000 });
});
afterEach(() => {
manager.destroy();
});
it('should create a session', () => {
const session = manager.createSession('stdio');
expect(session.id).toBeDefined();
expect(session.state).toBe('created');
expect(session.transport).toBe('stdio');
expect(session.isInitialized).toBe(false);
});
it('should initialize a session', () => {
const session = manager.createSession('stdio');
manager.initializeSession(session.id, {
protocolVersion: { major: 2024, minor: 11, patch: 5 },
capabilities: { tools: { listChanged: true } },
clientInfo: { name: 'test-client', version: '1.0.0' },
});
const updated = manager.getSession(session.id);
expect(updated?.isInitialized).toBe(true);
expect(updated?.state).toBe('ready');
expect(updated?.clientInfo?.name).toBe('test-client');
});
it('should close a session', () => {
const session = manager.createSession('stdio');
const result = manager.closeSession(session.id, 'test reason');
expect(result).toBe(true);
expect(manager.getSession(session.id)).toBeUndefined();
});
it('should get session metrics', () => {
manager.createSession('stdio');
manager.createSession('http');
const metrics = manager.getSessionMetrics();
expect(metrics.total).toBe(2);
});
it('should update session activity', () => {
const session = manager.createSession('stdio');
const originalTime = session.lastActivityAt;
// Small delay to ensure time difference
const result = manager.updateActivity(session.id);
expect(result).toBe(true);
});
});
describe('ConnectionPool', () => {
let pool: ReturnType<typeof createConnectionPool>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
pool = createConnectionPool(
{ maxConnections: 5, minConnections: 0, idleTimeout: 100, evictionRunInterval: 60000 },
logger,
'in-process'
);
});
afterEach(async () => {
// Release all connections first to allow fast drain
for (const conn of pool.getConnections()) {
if (conn.state === 'busy') {
pool.release(conn);
}
}
await pool.clear();
}, 5000);
it('should acquire a connection', async () => {
const connection = await pool.acquire();
expect(connection.id).toBeDefined();
expect(connection.state).toBe('busy');
});
it('should release a connection', async () => {
const connection = await pool.acquire();
pool.release(connection);
const stats = pool.getStats();
expect(stats.idleConnections).toBeGreaterThan(0);
});
it('should get stats', async () => {
const connection = await pool.acquire();
const stats = pool.getStats();
expect(stats.totalConnections).toBeGreaterThan(0);
expect(stats.totalAcquired).toBe(1);
// Release for cleanup
pool.release(connection);
});
it('should check health', () => {
// Pool is healthy when not shutting down and has >= minConnections (0 in this test)
expect(pool.isHealthy()).toBe(true);
});
});
describe('MCPServer', () => {
let server: ReturnType<typeof createMCPServer>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
server = createMCPServer(
{ name: 'Test Server', transport: 'in-process' },
logger
);
});
afterEach(async () => {
await server.stop();
});
it('should create server with config', () => {
expect(server).toBeDefined();
});
it('should register a tool', () => {
const result = server.registerTool({
name: 'test-tool',
description: 'Test tool',
inputSchema: { type: 'object' },
handler: async () => ({}),
});
expect(result).toBe(true);
});
it('should register multiple tools', () => {
const result = server.registerTools([
{
name: 'tool-1',
description: 'Tool 1',
inputSchema: { type: 'object' },
handler: async () => ({}),
},
{
name: 'tool-2',
description: 'Tool 2',
inputSchema: { type: 'object' },
handler: async () => ({}),
},
]);
expect(result.registered).toBe(2);
expect(result.failed).toHaveLength(0);
});
it('should start and stop', async () => {
await server.start();
const health = await server.getHealthStatus();
expect(health.healthy).toBe(true);
await server.stop();
const healthAfter = await server.getHealthStatus();
expect(healthAfter.healthy).toBe(false);
});
it('should get metrics', async () => {
await server.start();
const metrics = server.getMetrics();
expect(metrics.totalRequests).toBeDefined();
expect(metrics.activeSessions).toBeDefined();
});
it('should expose resource registry', () => {
const registry = server.getResourceRegistry();
expect(registry).toBeDefined();
expect(typeof registry.registerResource).toBe('function');
});
it('should expose prompt registry', () => {
const registry = server.getPromptRegistry();
expect(registry).toBeDefined();
expect(typeof registry.register).toBe('function');
});
it('should expose task manager', () => {
const taskManager = server.getTaskManager();
expect(taskManager).toBeDefined();
expect(typeof taskManager.createTask).toBe('function');
});
});
// ============================================================================
// MCP 2025-11-25 Features
// ============================================================================
describe('ResourceRegistry (MCP 2025-11-25)', () => {
let registry: ReturnType<typeof createResourceRegistry>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
registry = createResourceRegistry(logger);
});
it('should register a resource', () => {
const { resource, handler } = createTextResource(
'file://test.txt',
'Test File',
'Hello World'
);
const result = registry.registerResource(resource, handler);
expect(result).toBe(true);
expect(registry.hasResource('file://test.txt')).toBe(true);
});
it('should list resources with pagination', () => {
for (let i = 0; i < 5; i++) {
const { resource, handler } = createTextResource(
`file://test${i}.txt`,
`Test File ${i}`,
`Content ${i}`
);
registry.registerResource(resource, handler);
}
const result = registry.list(undefined, 3);
expect(result.resources.length).toBe(3);
expect(result.nextCursor).toBeDefined();
const nextResult = registry.list(result.nextCursor, 3);
expect(nextResult.resources.length).toBe(2);
});
it('should read resource content', async () => {
const { resource, handler } = createTextResource(
'file://test.txt',
'Test File',
'Hello World'
);
registry.registerResource(resource, handler);
const result = await registry.read('file://test.txt');
expect(result.contents[0].text).toBe('Hello World');
});
it('should subscribe to resource updates', () => {
const { resource, handler } = createTextResource(
'file://test.txt',
'Test File',
'Hello World'
);
registry.registerResource(resource, handler);
const callback = vi.fn();
const subscriptionId = registry.subscribe('file://test.txt', callback);
expect(subscriptionId).toBeDefined();
expect(registry.getSubscriptionCount('file://test.txt')).toBe(1);
});
it('should get stats', () => {
const { resource, handler } = createTextResource(
'file://test.txt',
'Test File',
'Hello World'
);
registry.registerResource(resource, handler);
const stats = registry.getStats();
expect(stats.totalResources).toBe(1);
});
});
describe('PromptRegistry (MCP 2025-11-25)', () => {
let registry: ReturnType<typeof createPromptRegistry>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
registry = createPromptRegistry(logger);
});
it('should register a prompt', () => {
const prompt = definePrompt(
'code_review',
'Review code for quality',
async (args) => [textMessage('user', `Review: ${args.code}`)],
{ arguments: [{ name: 'code', required: true }] }
);
const result = registry.register(prompt);
expect(result).toBe(true);
expect(registry.hasPrompt('code_review')).toBe(true);
});
it('should list prompts with pagination', () => {
for (let i = 0; i < 5; i++) {
const prompt = definePrompt(
`prompt_${i}`,
`Prompt ${i}`,
async () => [textMessage('user', `Message ${i}`)]
);
registry.register(prompt);
}
const result = registry.list(undefined, 3);
expect(result.prompts.length).toBe(3);
expect(result.nextCursor).toBeDefined();
});
it('should get prompt with arguments', async () => {
const prompt = definePrompt(
'greeting',
'Greet a user',
async (args) => [textMessage('user', `Hello, ${args.name}!`)],
{ arguments: [{ name: 'name', required: true }] }
);
registry.register(prompt);
const result = await registry.get('greeting', { name: 'World' });
expect(result.messages[0].content.type).toBe('text');
expect((result.messages[0].content as any).text).toBe('Hello, World!');
});
it('should validate required arguments', async () => {
const prompt = definePrompt(
'greeting',
'Greet a user',
async (args) => [textMessage('user', `Hello, ${args.name}!`)],
{ arguments: [{ name: 'name', required: true }] }
);
registry.register(prompt);
await expect(registry.get('greeting', {})).rejects.toThrow('Missing required argument');
});
it('should interpolate template strings', () => {
const template = 'Hello, {name}! Your score is {score}.';
const result = interpolate(template, { name: 'Alice', score: '100' });
expect(result).toBe('Hello, Alice! Your score is 100.');
});
});
describe('TaskManager (MCP 2025-11-25)', () => {
let taskManager: ReturnType<typeof createTaskManager>;
let logger: ILogger;
beforeEach(() => {
logger = createMockLogger();
taskManager = createTaskManager(logger, { cleanupInterval: 60000 });
});
afterEach(() => {
taskManager.destroy();
});
it('should create a task', () => {
const taskId = taskManager.createTask(async (reportProgress, signal) => {
return { result: 'success' };
});
expect(taskId).toBeDefined();
expect(taskId.startsWith('task-')).toBe(true);
});
it('should get task status', () => {
const taskId = taskManager.createTask(async () => 'done');
const status = taskManager.getTask(taskId);
expect(status).toBeDefined();
expect(status?.taskId).toBe(taskId);
});
it('should track task progress', async () => {
const taskId = taskManager.createTask(async (reportProgress) => {
reportProgress({ progress: 50, total: 100, message: 'Halfway' });
return 'done';
});
// Wait for task to complete
const result = await taskManager.waitForTask(taskId, 5000);
expect(result.state).toBe('completed');
});
it('should cancel a task', () => {
const taskId = taskManager.createTask(async (reportProgress, signal) => {
// Long running task
await new Promise((resolve, reject) => {
const timeout = setTimeout(resolve, 10000);
signal.addEventListener('abort', () => {
clearTimeout(timeout);
reject(new Error('Cancelled'));
});
});
});
const cancelled = taskManager.cancelTask(taskId, 'User requested');
expect(cancelled).toBe(true);
});
it('should get all tasks', () => {
taskManager.createTask(async () => 'task1');
taskManager.createTask(async () => 'task2');
const tasks = taskManager.getAllTasks();
expect(tasks.length).toBe(2);
});
it('should get stats', () => {
taskManager.createTask(async () => 'done');
const stats = taskManager.getStats();
expect(stats.totalTasks).toBeGreaterThan(0);
});
});
});