Skip to main content
Glama
test-helpers.ts14.7 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; // import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { tools } from '../../../src/tools/index.ts'; import { TEST_CONFIG, sleep, retryOperation } from '../setup.ts'; // Re-export commonly used functions export { sleep } from '../setup.ts'; import { logger } from '../../../src/utils/logger.ts'; import { z } from 'zod'; // Test server instance let testServer: Server | null = null; // Create test server for testing (simplified approach) export const createTestServer = (): Server => { if (testServer) { return testServer; } testServer = new Server( { name: 'float-mcp-test', version: '0.2.2', }, { capabilities: { tools: {}, resources: {}, prompts: {}, }, } ); // Register tool list handler testServer.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema), })), }; }); // Register tool call handler testServer.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; const tool = tools.find((t) => t.name === toolName); if (!tool) { throw new Error(`Tool ${toolName} not found`); } const result = await tool.handler(request.params.arguments || {}); // Extract data from ToolResponse if needed const responseData = result && typeof result === 'object' && 'data' in result ? result.data : result; return { content: [ { type: 'text', text: JSON.stringify(responseData, null, 2), }, ], }; }); return testServer; }; // Execute a tool directly using the tool handler export const executeToolWithRetry = async <T>( toolName: string, params: Record<string, any>, maxRetries: number = TEST_CONFIG.retryAttempts ): Promise<T> => { return retryOperation(async () => { // Add delay between calls to avoid rate limiting await sleep(TEST_CONFIG.apiCallDelay); const tool = tools.find((t) => t.name === toolName); if (!tool) { throw new Error(`Tool ${toolName} not found`); } const result = await tool.handler(params || {}); // Extract data from ToolResponse if needed const responseData = result && typeof result === 'object' && 'data' in result ? result.data : result; return responseData; }, maxRetries); }; // Execute a tool without retries (for testing error scenarios) export const executeTool = async <T>(toolName: string, params: Record<string, any>): Promise<T> => { return executeToolWithRetry(toolName, params, 1); }; // Get all available tools export const getAvailableTools = async (): Promise< Array<{ name: string; description: string; inputSchema: any }> > => { return tools.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema), })); }; // Validate tool response against schema export const validateToolResponse = <T>(response: any, schema: z.ZodSchema<T>): T => { try { return schema.parse(response); } catch (error) { if (error instanceof z.ZodError) { throw new Error(`Schema validation failed: ${error.message}`); } throw error; } }; // Test data generators export const generateTestProjectData = (overrides: Partial<any> = {}): Record<string, any> => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return { name: `Test Project ${timestamp}_${random}`, client_id: 1, start_date: '2024-01-01', end_date: '2024-12-31', notes: 'Test project created by integration tests', active: 1, ...overrides, }; }; export const generateTestPersonData = (overrides: Partial<any> = {}): Record<string, any> => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return { name: `Test Person ${timestamp}_${random}`, email: `test.person.${timestamp}@example.com`, job_title: 'Test Developer', active: 1, employee_type: 1, ...overrides, }; }; export const generateTestTaskData = (overrides: Partial<any> = {}): Record<string, any> => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return { name: `Test Task ${timestamp}_${random}`, project_id: 1, people_id: 1, // Required field for create-task start_date: '2024-01-01', end_date: '2024-01-31', notes: 'Test task created by integration tests', ...overrides, }; }; export const generateTestClientData = (overrides: Partial<any> = {}): Record<string, any> => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); return { name: `Test Client ${timestamp}_${random}`, notes: 'Test client created by integration tests', ...overrides, }; }; export const generateTestAllocationData = (overrides: Partial<any> = {}): Record<string, any> => { return { person_id: 1, project_id: 1, start_date: '2024-01-01', end_date: '2024-01-31', hours: 8, notes: 'Test allocation created by integration tests', ...overrides, }; }; // Optimized tool parameter generators export const generateManageEntityParams = ( entity_type: string, operation: string, overrides: Partial<any> = {} ): Record<string, any> => { const baseParams = { entity_type, operation, ...overrides, }; // Add entity-specific fields based on type and operation switch (entity_type) { case 'people': if (operation === 'create' || operation === 'update') { return { ...baseParams, ...generateTestPersonData(overrides), }; } break; case 'projects': if (operation === 'create' || operation === 'update') { return { ...baseParams, ...generateTestProjectData(overrides), }; } break; case 'tasks': if (operation === 'create' || operation === 'update') { return { ...baseParams, ...generateTestTaskData(overrides), }; } break; case 'clients': if (operation === 'create' || operation === 'update') { return { ...baseParams, ...generateTestClientData(overrides), }; } break; } return baseParams; }; export const generateManageProjectWorkflowParams = ( entity_type: string, operation: string, overrides: Partial<any> = {} ): Record<string, any> => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); const baseParams = { entity_type, operation, ...overrides, }; // Add workflow-specific fields switch (entity_type) { case 'phases': if (operation === 'create' || operation === 'update') { return { ...baseParams, name: `Test Phase ${timestamp}_${random}`, project_id: 1, start_date: '2024-01-01', end_date: '2024-01-31', color: '#3498db', ...overrides, }; } break; case 'milestones': if (operation === 'create' || operation === 'update') { return { ...baseParams, name: `Test Milestone ${timestamp}_${random}`, project_id: 1, date: '2024-01-15', notes: 'Test milestone created by integration tests', ...overrides, }; } break; case 'project-tasks': if (operation === 'create' || operation === 'update') { return { ...baseParams, name: `Test Project Task ${timestamp}_${random}`, project_id: 1, start_date: '2024-01-01', end_date: '2024-01-31', estimated_hours: 8, ...overrides, }; } break; case 'allocations': if (operation === 'create' || operation === 'update') { return { ...baseParams, person_id: 1, project_id: 1, start_date: '2024-01-01', end_date: '2024-01-31', hours: 8, notes: 'Test allocation created by integration tests', ...overrides, }; } break; } return baseParams; }; export const generateManageTimeTrackingParams = ( entity_type: string, operation: string, overrides: Partial<any> = {} ): Record<string, any> => { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 8); const baseParams = { entity_type, operation, ...overrides, }; // Add tracking-specific fields switch (entity_type) { case 'logged-time': if (operation === 'create' || operation === 'update') { return { ...baseParams, person_id: 1, project_id: 1, task_id: 1, hours: 8, date: '2024-01-01', notes: `Test logged time ${timestamp}_${random}`, ...overrides, }; } break; case 'timeoff': if (operation === 'create' || operation === 'update') { return { ...baseParams, person_id: 1, timeoff_type_id: 1, start_date: '2024-01-01', end_date: '2024-01-01', full_day: 1, notes: `Test timeoff ${timestamp}_${random}`, ...overrides, }; } break; case 'public-holidays': if (operation === 'create' || operation === 'update') { return { ...baseParams, name: `Test Public Holiday ${timestamp}_${random}`, date: '2024-12-25', country: 'US', ...overrides, }; } break; case 'team-holidays': if (operation === 'create' || operation === 'update') { return { ...baseParams, name: `Test Team Holiday ${timestamp}_${random}`, start_date: '2024-01-01', end_date: '2024-01-01', department_id: 1, ...overrides, }; } break; } return baseParams; }; export const generateReportParams = ( report_type: string, overrides: Partial<any> = {} ): Record<string, any> => { const baseParams = { report_type, start_date: '2024-01-01', end_date: '2024-01-31', ...overrides, }; // Add report-specific parameters switch (report_type) { case 'time-report': return { ...baseParams, person_id: 1, project_id: 1, group_by: 'person', include_billable: true, ...overrides, }; case 'project-report': return { ...baseParams, project_id: 1, include_allocations: true, include_time_entries: true, ...overrides, }; case 'people-utilization-report': return { ...baseParams, department_id: 1, include_contractors: true, utilization_threshold: 80, ...overrides, }; default: return baseParams; } }; // Helper to clean up test data using optimized tools export const cleanupTestDataOptimized = async ( entityType: string, entityId: number ): Promise<void> => { if (!TEST_CONFIG.enableRealApiCalls) { return; // Skip cleanup for mocked tests } try { // Use the optimized manage-entity tool for cleanup await executeTool('manage-entity', { entity_type: entityType === 'person' ? 'people' : entityType + 's', // Convert to plural operation: 'delete', id: entityId, }); logger.info(`Cleaned up test ${entityType} ${entityId} using optimized tools`); } catch (error) { logger.warn(`Failed to clean up test ${entityType} ${entityId}:`, error); } }; // Helper to clean up test data (legacy version for backward compatibility) export const cleanupTestData = async (entityType: string, entityId: number): Promise<void> => { if (!TEST_CONFIG.enableRealApiCalls) { return; // Skip cleanup for mocked tests } try { const deleteToolName = `delete-${entityType}`; const idParam = `${entityType}_id`; await executeTool(deleteToolName, { [idParam]: entityId }); logger.info(`Cleaned up test ${entityType} ${entityId}`); } catch (error) { logger.warn(`Failed to clean up test ${entityType} ${entityId}:`, error); } }; // Helper to wait for async operations export const waitFor = async ( condition: () => Promise<boolean>, timeout: number = 10000, interval: number = 500 ): Promise<void> => { const start = Date.now(); while (Date.now() - start < timeout) { if (await condition()) { return; } await sleep(interval); } throw new Error(`Condition not met within ${timeout}ms`); }; // Helper to check if a tool exists export const toolExists = (toolName: string): boolean => { return tools.some((tool) => tool.name === toolName); }; // Helper to get tool schema export const getToolSchema = (toolName: string): any => { const tool = tools.find((t) => t.name === toolName); if (!tool) { throw new Error(`Tool ${toolName} not found`); } return tool.inputSchema; }; // Helper to validate tool parameters export const validateToolParameters = <T>(toolName: string, params: any): T => { const schema = getToolSchema(toolName); return validateToolResponse(params, schema); }; // Error assertion helpers export const expectError = async ( operation: () => Promise<any>, expectedErrorMessage?: string ): Promise<Error> => { try { await operation(); throw new Error('Expected operation to throw an error'); } catch (error) { if (expectedErrorMessage && error instanceof Error) { expect(error.message).toContain(expectedErrorMessage); } return error as Error; } }; // Rate limiting helper export const withRateLimit = async <T>(operation: () => Promise<T>): Promise<T> => { await sleep(TEST_CONFIG.apiCallDelay); return operation(); }; // Batch operation helper export const executeBatch = async <T>( operations: Array<() => Promise<T>>, batchSize: number = TEST_CONFIG.maxConcurrentRequests ): Promise<T[]> => { const results: T[] = []; for (let i = 0; i < operations.length; i += batchSize) { const batch = operations.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map((op) => withRateLimit(op))); results.push(...batchResults); } return results; };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/asachs01/float-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server