BioMCP
by acashmoney
- tests
- e2e
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { analyzeActiveSite, analyzeActiveSiteSchema } from "../../src/tools/analyzeActiveSite.js";
import { searchDiseaseProteins, searchDiseaseProteinsSchema } from "../../src/tools/searchDiseaseProteins.js";
import { mockPdbData, mockUniprotData, mockDiseaseSearchResults } from "../mocks/pdbData";
import { jest } from '@jest/globals';
// Before all tests, mock the RCSB_PDB_SEARCH_API constant
beforeAll(() => {
// We'll use more direct mocking
jest.spyOn(global, 'fetch').mockImplementation((url, options) => {
// Convert URL to string if it's a URL object
const urlString = typeof url === 'string' ? url : url.toString();
if (urlString.includes('rcsbsearch/v2/query')) {
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({
result_set: [
{ identifier: '6LU7', title: 'SARS-CoV-2 main protease' },
{ identifier: '7L0D', title: 'SARS-CoV-2 spike protein' }
],
total_count: 2
}),
text: () => Promise.resolve(JSON.stringify({
result_set: [
{ identifier: '6LU7', title: 'SARS-CoV-2 main protease' },
{ identifier: '7L0D', title: 'SARS-CoV-2 spike protein' }
],
total_count: 2
}))
} as unknown as Response);
}
if (urlString.includes('/core/entry/')) {
// Extract PDB ID
const pdbId = urlString.split('/').pop()?.split('?')[0];
return Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({
struct: { title: `${pdbId} protein structure` },
rcsb_primary_citation: { title: `Structure of ${pdbId}`, journal_abbrev: 'Science', year: 2020 }
}),
text: () => Promise.resolve(JSON.stringify({
struct: { title: `${pdbId} protein structure` },
rcsb_primary_citation: { title: `Structure of ${pdbId}`, journal_abbrev: 'Science', year: 2020 }
}))
} as unknown as Response);
}
// Default fallback
return Promise.resolve({
ok: false,
status: 404,
text: () => Promise.resolve('Not found')
} as unknown as Response);
});
});
// Before the mock for makeApiRequest, add these constants
jest.mock('../../src/utils', () => {
return require('../../src/server/utils');
});
jest.mock('../../src/server/utils', () => ({
RCSB_PDB_DATA_API: 'https://data.rcsb.org/rest/v1',
UNIPROT_API_BASE: 'https://rest.uniprot.org/uniprotkb',
RCSB_PDB_SEARCH_API: 'https://search.rcsb.org/rcsbsearch/v2/query',
makeApiRequest: jest.fn(async (url: string, method?: string, body?: Record<string, unknown>) => {
// Handle REST API requests for PDB entries
if (url.includes('/core/entry/')) {
// Extract PDB ID and endpoint
const urlParts = url.split('/');
const pdbIdIndex = urlParts.indexOf('entry') + 1;
const pdbId = urlParts[pdbIdIndex]?.toUpperCase();
const endpoint = urlParts[pdbIdIndex + 1];
if (pdbId) {
// Handle specific endpoints
if (endpoint === 'struct_site') {
if (pdbId === '6LU7') {
return [
{
id: "AC1",
details: "ACTIVE SITE",
pdbx_evidence_code: "Software",
pdbx_site_details: "Catalytic dyad: HIS41, CYS145"
}
];
}
return [];
} else if (endpoint === 'struct_site_gen') {
return []; // Mock empty site gen data
} else if (endpoint === 'nonpolymer_entity' || endpoint === 'ligands') {
if (pdbId === '6LU7') {
return [
{
pdbx_entity_nonpoly: {
comp_id: "N3",
name: "Inhibitor N3"
}
}
];
}
return [];
} else if (endpoint === 'polymer_entity') {
if (pdbId === '6LU7') {
return [
{
rcsb_polymer_entity_container_identifiers: {
uniprot_ids: ["P0DTD1"]
}
}
];
}
return [];
} else {
// Basic entry data
if (pdbId === '6LU7') {
return {
struct: {
title: 'SARS-CoV-2 main protease in complex with an inhibitor N3'
},
rcsb_primary_citation: {
title: 'Crystal structure of SARS-CoV-2 main protease',
journal_abbrev: 'Science',
year: 2020
},
rcsb_entry_info: {
molecular_weight: 34371.26,
deposited_polymer_monomer_count: 306,
deposited_atom_count: 2486,
polymer_entity_count_protein: 1,
ligand_count: 3
}
};
} else if (pdbId === '7L0D') {
return {
struct: {
title: 'SARS-CoV-2 spike protein'
},
rcsb_primary_citation: {
title: 'Structure of SARS-CoV-2 spike protein',
journal_abbrev: 'Nature',
year: 2020
}
};
}
}
}
return null;
}
// Handle UniProt API requests
if (url.includes('uniprot.org')) {
const uniprotId = url.split('/').pop();
if (uniprotId === 'P0DTD1') {
return {
comments: [
{
commentType: "FUNCTION",
texts: [
{
value: "Cleaves the polyprotein at the 3C-like protease self-cleavage site."
}
]
}
]
};
}
return null;
}
// Handle search API requests
if (url.includes('rcsbsearch/v2/query') && method === 'POST') {
return {
result_set: [
{
identifier: '6LU7',
title: 'SARS-CoV-2 main protease in complex with an inhibitor N3'
},
{
identifier: '7L0D',
title: 'SARS-CoV-2 spike protein'
}
],
total_count: 2
};
}
return null;
})
}));
// Need to modify the server code to be importable for testing
// or create a test instance of the server
describe('MCP Server E2E Tests', () => {
let server: McpServer;
let client: Client;
let serverTransport: InMemoryTransport;
let clientTransport: InMemoryTransport;
beforeAll(async () => {
// Set up a test server instance
server = new McpServer({
name: "bio-mcp-test",
version: "0.1.0",
});
// Add tools directly from our implementation
server.tool(
"analyze-active-site",
"Analyze the active site of a protein structure",
analyzeActiveSiteSchema,
analyzeActiveSite
);
server.tool(
"search-disease-proteins",
"Search for proteins related to a disease",
searchDiseaseProteinsSchema,
searchDiseaseProteins
);
// Create a linked pair of in-memory transports
[clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
// Create the client
client = new Client({
name: "test-client",
version: "1.0.0"
});
// Connect client and server
await server.connect(serverTransport);
await client.connect(clientTransport);
});
afterAll(() => {
jest.restoreAllMocks();
// Add these to ensure proper cleanup
jest.useRealTimers();
jest.clearAllMocks();
});
it('should analyze active site for a valid PDB ID', async () => {
// Timeout to prevent infinite hanging
jest.setTimeout(5000);
const result = await client.callTool({
name: "analyze-active-site",
arguments: {
pdbId: "6LU7",
}
});
expect(result).toBeDefined();
// Type assertion to help TypeScript understand the structure
const content = result.content as Array<{type: string, text: string}>;
expect(content).toBeDefined();
expect(content.length).toBeGreaterThan(0);
expect(content[0].type).toBe("text");
expect(content[0].text).toContain("6LU7");
expect(content[0].text).toContain("SARS-CoV-2");
}, 5000); // Add explicit timeout
it('should handle invalid PDB IDs gracefully', async () => {
// Add timeout to prevent test from hanging
jest.setTimeout(5000);
const result = await client.callTool({
name: "analyze-active-site",
arguments: {
pdbId: "XXXX",
}
});
expect(result).toBeDefined();
// Type assertion to help TypeScript understand the structure
const content = result.content as Array<{type: string, text: string}>;
expect(content).toBeDefined();
expect(content.length).toBeGreaterThan(0);
expect(content[0].type).toBe("text");
expect(content[0].text).toContain("Failed to retrieve structure data");
}, 5000); // Add explicit timeout
it('should search for disease proteins', async () => {
// Add timeout to prevent test from hanging
jest.setTimeout(5000);
const result = await client.callTool({
name: "search-disease-proteins",
arguments: {
disease: "covid",
}
});
expect(result).toBeDefined();
// Type assertion to help TypeScript understand the structure
const content = result.content as Array<{type: string, text: string}>;
expect(content).toBeDefined();
expect(content.length).toBeGreaterThan(0);
expect(content[0].type).toBe("text");
expect(content[0].text).toContain("Found");
expect(content[0].text).toContain("PDB ID");
}, 5000); // Add explicit timeout
});