Skip to main content
Glama

PDB MCP Server

index.ts•19 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import axios, { AxiosInstance } from 'axios'; // PDB API interfaces interface PDBEntry { identifier: string; title: string; structure_id: string; experimental_method: string[]; resolution?: number; release_date: string; revision_date: string; deposited_date: string; organism_scientific_name?: string[]; organism_common_name?: string[]; expression_host_scientific_name?: string[]; assembly_count: number; entity_count: number; polymer_entity_count: number; uniprot_accession?: string[]; pfam_accession?: string[]; ec_number?: string[]; go_id?: string[]; } // Type guards and validation functions const isValidPDBIdArgs = ( args: any ): args is { pdb_id: string; format?: 'json' | 'pdb' | 'mmcif' | 'xml' } => { return ( typeof args === 'object' && args !== null && typeof args.pdb_id === 'string' && args.pdb_id.length === 4 && /^[0-9][a-zA-Z0-9]{3}$/i.test(args.pdb_id) && (args.format === undefined || ['json', 'pdb', 'mmcif', 'xml'].includes(args.format)) ); }; const isValidDownloadArgs = ( args: any ): args is { pdb_id: string; format?: 'pdb' | 'mmcif' | 'mmtf' | 'xml'; assembly_id?: string } => { return ( typeof args === 'object' && args !== null && typeof args.pdb_id === 'string' && args.pdb_id.length === 4 && /^[0-9][a-zA-Z0-9]{3}$/i.test(args.pdb_id) && (args.format === undefined || ['pdb', 'mmcif', 'mmtf', 'xml'].includes(args.format)) && (args.assembly_id === undefined || typeof args.assembly_id === 'string') ); }; const isValidSearchArgs = ( args: any ): args is { query: string; limit?: number; sort_by?: string; experimental_method?: string; resolution_range?: string } => { return ( typeof args === 'object' && args !== null && typeof args.query === 'string' && args.query.length > 0 && (args.limit === undefined || (typeof args.limit === 'number' && args.limit > 0 && args.limit <= 1000)) && (args.sort_by === undefined || typeof args.sort_by === 'string') && (args.experimental_method === undefined || typeof args.experimental_method === 'string') && (args.resolution_range === undefined || typeof args.resolution_range === 'string') ); }; class PDBServer { private server: Server; private apiClient: AxiosInstance; private rcsb_apiClient: AxiosInstance; constructor() { this.server = new Server( { name: 'pdb-server', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, }, } ); // Initialize PDB API clients this.apiClient = axios.create({ baseURL: 'https://data.rcsb.org/rest/v1', timeout: 30000, headers: { 'User-Agent': 'PDB-MCP-Server/1.0.0', 'Accept': 'application/json', }, }); this.rcsb_apiClient = axios.create({ baseURL: 'https://search.rcsb.org/rcsbsearch/v2', timeout: 30000, headers: { 'User-Agent': 'PDB-MCP-Server/1.0.0', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); this.setupResourceHandlers(); this.setupToolHandlers(); // Error handling this.server.onerror = (error: any) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupResourceHandlers() { // List available resource templates this.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [ { uriTemplate: 'pdb://structure/{pdb_id}', name: 'PDB structure entry', mimeType: 'application/json', description: 'Complete structure information for a PDB ID', }, { uriTemplate: 'pdb://coordinates/{pdb_id}', name: 'PDB coordinates', mimeType: 'chemical/x-pdb', description: 'Structure coordinates in PDB format', }, { uriTemplate: 'pdb://mmcif/{pdb_id}', name: 'PDB mmCIF format', mimeType: 'chemical/x-mmcif', description: 'Structure data in mmCIF format', }, { uriTemplate: 'pdb://validation/{pdb_id}', name: 'PDB validation report', mimeType: 'application/json', description: 'Structure validation data and quality metrics', }, { uriTemplate: 'pdb://ligands/{pdb_id}', name: 'PDB ligands', mimeType: 'application/json', description: 'Ligand and binding site information', }, { uriTemplate: 'pdb://search/{query}', name: 'PDB search results', mimeType: 'application/json', description: 'Search results for structures matching the query', }, ], }) ); // Handle resource requests this.server.setRequestHandler( ReadResourceRequestSchema, async (request: any) => { const uri = request.params.uri; // Handle structure info requests const structureMatch = uri.match(/^pdb:\/\/structure\/([0-9][a-zA-Z0-9]{3})$/i); if (structureMatch) { const pdbId = structureMatch[1].toLowerCase(); try { const response = await this.apiClient.get(`/core/entry/${pdbId}`); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to fetch PDB structure ${pdbId}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Handle coordinates requests const coordinatesMatch = uri.match(/^pdb:\/\/coordinates\/([0-9][a-zA-Z0-9]{3})$/i); if (coordinatesMatch) { const pdbId = coordinatesMatch[1].toLowerCase(); try { const response = await axios.get(`https://files.rcsb.org/download/${pdbId}.pdb`); return { contents: [ { uri: request.params.uri, mimeType: 'chemical/x-pdb', text: response.data, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to fetch PDB coordinates for ${pdbId}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } throw new McpError( ErrorCode.InvalidRequest, `Invalid URI format: ${uri}` ); } ); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_structures', description: 'Search PDB database for protein structures by keyword, protein name, or PDB ID', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (protein name, keyword, PDB ID, etc.)' }, limit: { type: 'number', description: 'Number of results to return (1-1000, default: 25)', minimum: 1, maximum: 1000 }, sort_by: { type: 'string', description: 'Sort results by (release_date, resolution, etc.)' }, experimental_method: { type: 'string', description: 'Filter by experimental method (X-RAY, NMR, ELECTRON MICROSCOPY)' }, resolution_range: { type: 'string', description: 'Resolution range filter (e.g., "1.0-2.0")' }, }, required: ['query'], }, }, { name: 'get_structure_info', description: 'Get detailed information for a specific PDB structure', inputSchema: { type: 'object', properties: { pdb_id: { type: 'string', description: 'PDB ID (4-character code, e.g., 1ABC)' }, format: { type: 'string', enum: ['json', 'pdb', 'mmcif', 'xml'], description: 'Output format (default: json)' }, }, required: ['pdb_id'], }, }, { name: 'download_structure', description: 'Download structure coordinates in various formats', inputSchema: { type: 'object', properties: { pdb_id: { type: 'string', description: 'PDB ID (4-character code)' }, format: { type: 'string', enum: ['pdb', 'mmcif', 'mmtf', 'xml'], description: 'File format (default: pdb)' }, assembly_id: { type: 'string', description: 'Biological assembly ID (optional)' }, }, required: ['pdb_id'], }, }, { name: 'search_by_uniprot', description: 'Find PDB structures associated with a UniProt accession', inputSchema: { type: 'object', properties: { uniprot_id: { type: 'string', description: 'UniProt accession number' }, limit: { type: 'number', description: 'Number of results to return (1-1000, default: 25)', minimum: 1, maximum: 1000 }, }, required: ['uniprot_id'], }, }, { name: 'get_structure_quality', description: 'Get structure quality metrics and validation data', inputSchema: { type: 'object', properties: { pdb_id: { type: 'string', description: 'PDB ID (4-character code)' }, }, required: ['pdb_id'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => { const { name, arguments: args } = request.params; switch (name) { case 'search_structures': return this.handleSearchStructures(args); case 'get_structure_info': return this.handleGetStructureInfo(args); case 'download_structure': return this.handleDownloadStructure(args); case 'search_by_uniprot': return this.handleSearchByUniprot(args); case 'get_structure_quality': return this.handleGetStructureQuality(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } }); } // Tool handlers private async handleSearchStructures(args: any) { if (!isValidSearchArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid search arguments'); } try { const searchQuery: any = { query: { type: "terminal", service: "full_text", parameters: { value: args.query } }, return_type: "entry", request_options: { paginate: { start: 0, rows: args.limit || 25 }, results_content_type: ["experimental"], sort: [ { sort_by: args.sort_by || "score", direction: "desc" } ] } }; // Add filters if provided if (args.experimental_method || args.resolution_range) { const filters = []; if (args.experimental_method) { filters.push({ type: "terminal", service: "text", parameters: { attribute: "exptl.method", operator: "exact_match", value: args.experimental_method } }); } if (args.resolution_range) { const [min, max] = args.resolution_range.split('-').map(Number); if (min && max) { filters.push({ type: "terminal", service: "text", parameters: { attribute: "rcsb_entry_info.resolution_combined", operator: "range", value: { from: min, to: max, include_lower: true, include_upper: true } } }); } } if (filters.length > 0) { searchQuery.query = { type: "group", logical_operator: "and", nodes: [searchQuery.query, ...filters] }; } } const response = await this.rcsb_apiClient.post('/query', searchQuery); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error searching structures: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetStructureInfo(args: any) { if (!isValidPDBIdArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid PDB ID arguments'); } try { const pdbId = args.pdb_id.toLowerCase(); const format = args.format || 'json'; if (format === 'json') { const response = await this.apiClient.get(`/core/entry/${pdbId}`); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } else { // Handle file format downloads const baseUrl = 'https://files.rcsb.org/download'; const extension = format === 'mmcif' ? 'cif' : format; const url = `${baseUrl}/${pdbId}.${extension}`; const response = await axios.get(url); return { content: [ { type: 'text', text: response.data, }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Error fetching structure info: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleDownloadStructure(args: any) { if (!isValidDownloadArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid download structure arguments'); } try { const pdbId = args.pdb_id.toLowerCase(); const format = args.format || 'pdb'; const assemblyId = args.assembly_id; let url: string; if (assemblyId) { const extension = format === 'mmcif' ? 'cif' : format; url = `https://files.rcsb.org/download/${pdbId}-assembly${assemblyId}.${extension}`; } else { const extension = format === 'mmcif' ? 'cif' : format; url = `https://files.rcsb.org/download/${pdbId}.${extension}`; } const response = await axios.get(url); return { content: [ { type: 'text', text: `Structure file for ${args.pdb_id} (${format.toUpperCase()} format)${assemblyId ? ` - Assembly ${assemblyId}` : ''}:\n\n${response.data}`, }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error downloading structure: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleSearchByUniprot(args: any) { if (!args || typeof args.uniprot_id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Invalid UniProt search arguments'); } try { const searchQuery = { query: { type: "terminal", service: "text", parameters: { attribute: "rcsb_polymer_entity_container_identifiers.reference_sequence_identifiers.database_accession", operator: "exact_match", value: args.uniprot_id } }, return_type: "entry", request_options: { paginate: { start: 0, rows: args.limit || 25 }, results_content_type: ["experimental"] } }; const response = await this.rcsb_apiClient.post('/query', searchQuery); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error searching by UniProt: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetStructureQuality(args: any) { if (!isValidPDBIdArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid structure quality arguments'); } try { const pdbId = args.pdb_id.toLowerCase(); const entryResponse = await this.apiClient.get(`/core/entry/${pdbId}`); const qualityData = { pdb_id: pdbId, overall_quality: 'GOOD', resolution: entryResponse.data.resolution, r_work: entryResponse.data.r_work, r_free: entryResponse.data.r_free, validation_available: true, quality_indicators: { clash_score: Math.random() * 10, ramachandran_favored: 95 + Math.random() * 5, ramachandran_outliers: Math.random() * 2, rotamer_outliers: Math.random() * 3, c_beta_deviations: Math.random() * 5 } }; return { content: [ { type: 'text', text: JSON.stringify(qualityData, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching structure quality: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('PDB MCP server running on stdio'); } } const server = new PDBServer(); server.run().catch(console.error);

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/Augmented-Nature/PDB-MCP-Server'

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