import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { spawn, ChildProcess } from 'child_process';
import { testSequences } from '../fixtures/sequences';
describe('MCP Server Integration', () => {
let serverProcess: ChildProcess;
let isServerReady = false;
const sendMCPRequest = async (request: any): Promise<any> => {
return new Promise((resolve, reject) => {
if (!serverProcess.stdin || !serverProcess.stdout) {
reject(new Error('Server process not available'));
return;
}
let responseData = '';
const onData = (data: Buffer) => {
responseData += data.toString();
try {
const response = JSON.parse(responseData);
serverProcess.stdout!.off('data', onData);
resolve(response);
} catch (e) {
// Continue collecting data if JSON is incomplete
}
};
serverProcess.stdout.on('data', onData);
// Send request
serverProcess.stdin.write(JSON.stringify(request) + '\n');
// Timeout after 30 seconds
setTimeout(() => {
serverProcess.stdout!.off('data', onData);
reject(new Error('Request timeout'));
}, 30000);
});
};
beforeAll(async () => {
// Start the MCP server
serverProcess = spawn('node', ['dist/server.js'], {
cwd: process.cwd(),
stdio: ['pipe', 'pipe', 'pipe']
});
// Wait for server to be ready
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Server startup timeout'));
}, 10000);
serverProcess.on('spawn', () => {
clearTimeout(timeout);
isServerReady = true;
resolve();
});
serverProcess.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
}, 15000);
afterAll(async () => {
if (serverProcess) {
serverProcess.kill();
}
});
describe('Server lifecycle', () => {
it('should start server successfully', () => {
expect(isServerReady).toBe(true);
expect(serverProcess.pid).toBeDefined();
});
});
describe('Tool listing', () => {
it('should list all available tools', async () => {
const request = {
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: {}
};
const response = await sendMCPRequest(request);
expect(response.jsonrpc).toBe('2.0');
expect(response.id).toBe(1);
expect(response.result).toBeDefined();
expect(response.result.tools).toHaveLength(6);
const toolNames = response.result.tools.map((tool: any) => tool.name);
expect(toolNames).toContain('generate_dna_sequence');
expect(toolNames).toContain('generate_protein_sequence');
expect(toolNames).toContain('mutate_sequence');
expect(toolNames).toContain('evolve_sequence');
expect(toolNames).toContain('simulate_phylogeny');
});
it('should have proper tool definitions', async () => {
const request = {
jsonrpc: '2.0',
id: 2,
method: 'tools/list',
params: {}
};
const response = await sendMCPRequest(request);
response.result.tools.forEach((tool: any) => {
expect(tool.name).toBeDefined();
expect(tool.description).toBeDefined();
expect(tool.inputSchema).toBeDefined();
expect(tool.inputSchema.type).toBe('object');
expect(tool.inputSchema.properties).toBeDefined();
});
});
});
describe('DNA Generation Tool Integration', () => {
it('should generate DNA sequences via MCP', async () => {
const request = {
jsonrpc: '2.0',
id: 3,
method: 'tools/call',
params: {
name: 'generate_dna_sequence',
arguments: {
length: 100,
gcContent: 0.6,
count: 2,
seed: 12345
}
}
};
const response = await sendMCPRequest(request);
expect(response.jsonrpc).toBe('2.0');
expect(response.id).toBe(3);
expect(response.result).toBeDefined();
expect(response.result.content).toHaveLength(1);
const data = JSON.parse(response.result.content[0].text);
expect(data.statistics.totalSequences).toBe(2);
expect(data.statistics.averageLength).toBe(100);
});
it('should handle invalid DNA generation parameters', async () => {
const request = {
jsonrpc: '2.0',
id: 4,
method: 'tools/call',
params: {
name: 'generate_dna_sequence',
arguments: {
// Missing required 'length' parameter
gcContent: 0.5
}
}
};
const response = await sendMCPRequest(request);
expect(response.error).toBeDefined();
});
});
describe('Protein Generation Tool Integration', () => {
it('should generate protein sequences via MCP', async () => {
const request = {
jsonrpc: '2.0',
id: 5,
method: 'tools/call',
params: {
name: 'generate_protein_sequence',
arguments: {
length: 50,
model: 'hydrophobic-bias',
seed: 12345
}
}
};
const response = await sendMCPRequest(request);
expect(response.result).toBeDefined();
const data = JSON.parse(response.result.content[0].text);
expect(data.statistics.model).toBe('hydrophobic-bias');
expect(data.statistics.averageLength).toBe(50);
});
});
describe('Mutation Tool Integration', () => {
it('should mutate sequences via MCP', async () => {
const request = {
jsonrpc: '2.0',
id: 6,
method: 'tools/call',
params: {
name: 'mutate_sequence',
arguments: {
sequence: testSequences.dna.short,
sequenceType: 'dna',
substitutionRate: 0.1,
iterations: 3,
seed: 12345
}
}
};
const response = await sendMCPRequest(request);
expect(response.result).toBeDefined();
const data = JSON.parse(response.result.content[0].text);
expect(data.statistics.sequenceType).toBe('dna');
expect(data.statistics.totalIterations).toBe(3);
expect(data.mutations).toHaveLength(4); // Original + 3 iterations
});
});
describe('Evolution Tool Integration', () => {
it('should evolve sequences via MCP', async () => {
const request = {
jsonrpc: '2.0',
id: 7,
method: 'tools/call',
params: {
name: 'evolve_sequence',
arguments: {
sequence: testSequences.dna.short,
generations: 5,
populationSize: 10,
mutationRate: 0.05,
seed: 12345
}
}
};
const response = await sendMCPRequest(request);
expect(response.result).toBeDefined();
const data = JSON.parse(response.result.content[0].text);
expect(data.summary.totalGenerations).toBe(5);
expect(data.summary.populationSize).toBe(10);
expect(data.evolutionHistory).toBeDefined();
});
});
describe('Phylogeny Tool Integration', () => {
it('should simulate phylogeny via MCP', async () => {
const request = {
jsonrpc: '2.0',
id: 8,
method: 'tools/call',
params: {
name: 'simulate_phylogeny',
arguments: {
rootSequence: testSequences.dna.medium,
numTaxa: 4,
mutationRate: 0.1,
seed: 12345
}
}
};
const response = await sendMCPRequest(request);
expect(response.result).toBeDefined();
const data = JSON.parse(response.result.content[0].text);
expect(data.parameters.numTaxa).toBe(4);
expect(data.treeStatistics.numTaxa).toBe(4);
expect(data.sequences).toBeDefined();
expect(Object.keys(data.sequences)).toHaveLength(4);
});
});
describe('Error handling', () => {
it('should handle unknown tool calls', async () => {
const request = {
jsonrpc: '2.0',
id: 9,
method: 'tools/call',
params: {
name: 'nonexistent_tool',
arguments: {}
}
};
const response = await sendMCPRequest(request);
expect(response.error).toBeDefined();
expect(response.error.message).toContain('Unknown tool');
});
it('should handle missing arguments', async () => {
const request = {
jsonrpc: '2.0',
id: 10,
method: 'tools/call',
params: {
name: 'generate_dna_sequence'
// Missing arguments
}
};
const response = await sendMCPRequest(request);
expect(response.error).toBeDefined();
expect(response.error.message).toContain('arguments are required');
});
it('should handle malformed JSON-RPC requests', async () => {
const request = {
// Missing required jsonrpc field
id: 11,
method: 'tools/list',
params: {}
};
try {
await sendMCPRequest(request);
fail('Should have thrown an error');
} catch (error) {
// Expected to fail
expect(error).toBeDefined();
}
});
});
describe('Performance and reliability', () => {
it('should handle multiple concurrent requests', async () => {
const requests = Array.from({ length: 5 }, (_, i) => ({
jsonrpc: '2.0',
id: 100 + i,
method: 'tools/call',
params: {
name: 'generate_dna_sequence',
arguments: {
length: 50,
seed: 12345 + i
}
}
}));
const responses = await Promise.all(
requests.map(req => sendMCPRequest(req))
);
expect(responses).toHaveLength(5);
responses.forEach((response, i) => {
expect(response.id).toBe(100 + i);
expect(response.result).toBeDefined();
});
}, 60000);
it('should handle large sequence generation', async () => {
const request = {
jsonrpc: '2.0',
id: 200,
method: 'tools/call',
params: {
name: 'generate_dna_sequence',
arguments: {
length: 10000,
count: 5,
seed: 12345
}
}
};
const response = await sendMCPRequest(request);
expect(response.result).toBeDefined();
const data = JSON.parse(response.result.content[0].text);
expect(data.statistics.totalSequences).toBe(5);
expect(data.statistics.averageLength).toBe(10000);
}, 30000);
});
describe('Data consistency', () => {
it('should produce consistent results with same seed', async () => {
const request = {
jsonrpc: '2.0',
id: 300,
method: 'tools/call',
params: {
name: 'generate_dna_sequence',
arguments: {
length: 100,
seed: 999999
}
}
};
const response1 = await sendMCPRequest({ ...request, id: 301 });
const response2 = await sendMCPRequest({ ...request, id: 302 });
expect(response1.result.content[0].text).toBe(response2.result.content[0].text);
});
it('should maintain sequence validity across all operations', async () => {
// Generate -> Mutate -> Evolve pipeline
const generateRequest = {
jsonrpc: '2.0',
id: 400,
method: 'tools/call',
params: {
name: 'generate_dna_sequence',
arguments: {
length: 200,
seed: 12345
}
}
};
const generateResponse = await sendMCPRequest(generateRequest);
const generateData = JSON.parse(generateResponse.result.content[0].text);
const initialSequence = generateData.sequences.match(/\n([ATGC]+)/)[1];
const mutateRequest = {
jsonrpc: '2.0',
id: 401,
method: 'tools/call',
params: {
name: 'mutate_sequence',
arguments: {
sequence: initialSequence,
sequenceType: 'dna',
substitutionRate: 0.1,
seed: 12345
}
}
};
const mutateResponse = await sendMCPRequest(mutateRequest);
const mutateData = JSON.parse(mutateResponse.result.content[0].text);
const mutatedSequence = mutateData.mutations[1].sequence;
const evolveRequest = {
jsonrpc: '2.0',
id: 402,
method: 'tools/call',
params: {
name: 'evolve_sequence',
arguments: {
sequence: mutatedSequence,
generations: 3,
populationSize: 5,
mutationRate: 0.05,
seed: 12345
}
}
};
const evolveResponse = await sendMCPRequest(evolveRequest);
const evolveData = JSON.parse(evolveResponse.result.content[0].text);
// All sequences should be valid DNA
expect(initialSequence).toMatch(/^[ATGC]+$/);
expect(mutatedSequence).toMatch(/^[ATGC]+$/);
expect(evolveData.summary.finalBestSequence).toMatch(/^[ATGC]+$/);
}, 45000);
});
});