Skip to main content
Glama
index.ts27 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'; // STRING API interfaces interface ProteinInteraction { stringId_A: string; stringId_B: string; preferredName_A: string; preferredName_B: string; ncbiTaxonId: number; score: number; nscore: number; fscore: number; pscore: number; ascore: number; escore: number; dscore: number; tscore: number; } interface NetworkNode { stringId: string; preferredName: string; protein_size: number; annotation: string; } interface EnrichmentTerm { category: string; term: string; number_of_genes: number; number_of_genes_in_background: number; ncbiTaxonId: number; inputGenes: string; preferredNames: string; pvalue: number; pvalue_fdr: number; description: string; } interface HomologyResult { stringId: string; ncbiTaxonId: number; taxonName: string; preferredName: string; annotation: string; } interface ProteinAnnotation { stringId: string; preferredName: string; ncbiTaxonId: number; annotation: string; protein_size: number; } // Type guards and validation functions const isValidProteinArgs = ( args: any ): args is { protein_id: string; species?: string; limit?: number; required_score?: number } => { return ( typeof args === 'object' && args !== null && typeof args.protein_id === 'string' && args.protein_id.length > 0 && (args.species === undefined || typeof args.species === 'string') && (args.limit === undefined || (typeof args.limit === 'number' && args.limit > 0 && args.limit <= 2000)) && (args.required_score === undefined || (typeof args.required_score === 'number' && args.required_score >= 0 && args.required_score <= 1000)) ); }; const isValidNetworkArgs = ( args: any ): args is { protein_ids: string[]; species?: string; network_type?: string; add_nodes?: number; required_score?: number } => { return ( typeof args === 'object' && args !== null && Array.isArray(args.protein_ids) && args.protein_ids.length > 0 && args.protein_ids.every((id: any) => typeof id === 'string') && (args.species === undefined || typeof args.species === 'string') && (args.network_type === undefined || ['functional', 'physical'].includes(args.network_type)) && (args.add_nodes === undefined || (typeof args.add_nodes === 'number' && args.add_nodes >= 0 && args.add_nodes <= 100)) && (args.required_score === undefined || (typeof args.required_score === 'number' && args.required_score >= 0 && args.required_score <= 1000)) ); }; const isValidEnrichmentArgs = ( args: any ): args is { protein_ids: string[]; species?: string; background_string_identifiers?: string[] } => { return ( typeof args === 'object' && args !== null && Array.isArray(args.protein_ids) && args.protein_ids.length > 0 && args.protein_ids.every((id: any) => typeof id === 'string') && (args.species === undefined || typeof args.species === 'string') && (args.background_string_identifiers === undefined || (Array.isArray(args.background_string_identifiers) && args.background_string_identifiers.every((id: any) => typeof id === 'string'))) ); }; const isValidHomologyArgs = ( args: any ): args is { protein_id: string; species?: string; target_species?: string[] } => { return ( typeof args === 'object' && args !== null && typeof args.protein_id === 'string' && args.protein_id.length > 0 && (args.species === undefined || typeof args.species === 'string') && (args.target_species === undefined || (Array.isArray(args.target_species) && args.target_species.every((sp: any) => typeof sp === 'string'))) ); }; const isValidSearchArgs = ( args: any ): args is { query: string; species?: string; limit?: number } => { return ( typeof args === 'object' && args !== null && typeof args.query === 'string' && args.query.length > 0 && (args.species === undefined || typeof args.species === 'string') && (args.limit === undefined || (typeof args.limit === 'number' && args.limit > 0 && args.limit <= 100)) ); }; const isValidPathArgs = ( args: any ): args is { protein_a: string; protein_b: string; species?: string; max_path_length?: number } => { return ( typeof args === 'object' && args !== null && typeof args.protein_a === 'string' && args.protein_a.length > 0 && typeof args.protein_b === 'string' && args.protein_b.length > 0 && (args.species === undefined || typeof args.species === 'string') && (args.max_path_length === undefined || (typeof args.max_path_length === 'number' && args.max_path_length >= 1 && args.max_path_length <= 5)) ); }; class StringServer { private server: Server; private apiClient: AxiosInstance; constructor() { this.server = new Server( { name: 'string-server', version: '1.0.0', }, { capabilities: { resources: {}, tools: {}, }, } ); // Initialize STRING API client this.apiClient = axios.create({ baseURL: 'https://string-db.org/api', timeout: 30000, headers: { 'User-Agent': 'STRING-MCP-Server/1.0.0', 'Accept': 'text/plain', }, }); 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: 'string://network/{protein_ids}', name: 'STRING protein network', mimeType: 'application/json', description: 'Protein interaction network data for specified proteins', }, { uriTemplate: 'string://enrichment/{protein_ids}', name: 'STRING functional enrichment', mimeType: 'application/json', description: 'Functional enrichment analysis results for protein set', }, { uriTemplate: 'string://interactions/{protein_id}', name: 'STRING protein interactions', mimeType: 'application/json', description: 'Direct interaction partners for a specific protein', }, { uriTemplate: 'string://homologs/{protein_id}', name: 'STRING protein homologs', mimeType: 'application/json', description: 'Homologous proteins across different species', }, { uriTemplate: 'string://annotations/{protein_id}', name: 'STRING protein annotations', mimeType: 'application/json', description: 'Detailed protein annotations and functional information', }, { uriTemplate: 'string://species/{taxon_id}', name: 'STRING species information', mimeType: 'application/json', description: 'Species-specific data and protein counts', }, ], }) ); // Handle resource requests this.server.setRequestHandler( ReadResourceRequestSchema, async (request: any) => { const uri = request.params.uri; // Handle network requests const networkMatch = uri.match(/^string:\/\/network\/(.+)$/); if (networkMatch) { const proteinIds = networkMatch[1].split(','); try { const result = await this.handleGetInteractionNetwork({ protein_ids: proteinIds }); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: result.content[0].text, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to fetch protein network: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Handle interaction requests const interactionMatch = uri.match(/^string:\/\/interactions\/(.+)$/); if (interactionMatch) { const proteinId = interactionMatch[1]; try { const result = await this.handleGetProteinInteractions({ protein_id: proteinId }); return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: result.content[0].text, }, ], }; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to fetch protein interactions: ${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: 'get_protein_interactions', description: 'Get direct interaction partners for a specific protein', inputSchema: { type: 'object', properties: { protein_id: { type: 'string', description: 'Protein identifier (gene name, UniProt ID, or STRING ID)' }, species: { type: 'string', description: 'Species name or NCBI taxonomy ID (default: 9606 for human)' }, limit: { type: 'number', description: 'Maximum number of interactions to return (default: 10)', minimum: 1, maximum: 2000 }, required_score: { type: 'number', description: 'Minimum interaction confidence score (0-1000, default: 400)', minimum: 0, maximum: 1000 }, }, required: ['protein_id'], }, }, { name: 'get_interaction_network', description: 'Build and analyze protein interaction network for multiple proteins', inputSchema: { type: 'object', properties: { protein_ids: { type: 'array', items: { type: 'string' }, description: 'List of protein identifiers' }, species: { type: 'string', description: 'Species name or NCBI taxonomy ID (default: 9606 for human)' }, network_type: { type: 'string', enum: ['functional', 'physical'], description: 'Type of network to build (default: functional)' }, add_nodes: { type: 'number', description: 'Number of additional interacting proteins to add (default: 0)', minimum: 0, maximum: 100 }, required_score: { type: 'number', description: 'Minimum interaction confidence score (0-1000, default: 400)', minimum: 0, maximum: 1000 }, }, required: ['protein_ids'], }, }, { name: 'get_functional_enrichment', description: 'Perform functional enrichment analysis on a set of proteins', inputSchema: { type: 'object', properties: { protein_ids: { type: 'array', items: { type: 'string' }, description: 'List of protein identifiers' }, species: { type: 'string', description: 'Species name or NCBI taxonomy ID (default: 9606 for human)' }, background_string_identifiers: { type: 'array', items: { type: 'string' }, description: 'Background protein set for enrichment (optional)' }, }, required: ['protein_ids'], }, }, { name: 'get_protein_annotations', description: 'Get detailed annotations and functional information for proteins', inputSchema: { type: 'object', properties: { protein_ids: { type: 'array', items: { type: 'string' }, description: 'List of protein identifiers' }, species: { type: 'string', description: 'Species name or NCBI taxonomy ID (default: 9606 for human)' }, }, required: ['protein_ids'], }, }, { name: 'find_homologs', description: 'Find homologous proteins across different species', inputSchema: { type: 'object', properties: { protein_id: { type: 'string', description: 'Protein identifier (gene name, UniProt ID, or STRING ID)' }, species: { type: 'string', description: 'Source species name or NCBI taxonomy ID (default: 9606 for human)' }, target_species: { type: 'array', items: { type: 'string' }, description: 'Target species to search for homologs (optional)' }, }, required: ['protein_id'], }, }, { name: 'search_proteins', description: 'Search for proteins by name or identifier across species', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (protein name, gene name, or identifier)' }, species: { type: 'string', description: 'Species name or NCBI taxonomy ID (optional)' }, limit: { type: 'number', description: 'Maximum number of results (default: 10)', minimum: 1, maximum: 100 }, }, required: ['query'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => { const { name, arguments: args } = request.params; switch (name) { case 'get_protein_interactions': return this.handleGetProteinInteractions(args); case 'get_interaction_network': return this.handleGetInteractionNetwork(args); case 'get_functional_enrichment': return this.handleGetFunctionalEnrichment(args); case 'get_protein_annotations': return this.handleGetProteinAnnotations(args); case 'find_homologs': return this.handleFindHomologs(args); case 'search_proteins': return this.handleSearchProteins(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } }); } // Utility methods private parseTsvData<T>(tsvData: string): T[] { const lines = tsvData.trim().split('\n'); if (lines.length < 2) return []; const headers = lines[0].split('\t'); const results: T[] = []; for (let i = 1; i < lines.length; i++) { const values = lines[i].split('\t'); const obj: any = {}; headers.forEach((header, index) => { const value = values[index] || ''; // Convert numeric fields if (['score', 'nscore', 'fscore', 'pscore', 'ascore', 'escore', 'dscore', 'tscore', 'ncbiTaxonId', 'protein_size', 'number_of_genes', 'number_of_genes_in_background', 'pvalue', 'pvalue_fdr'].includes(header)) { obj[header] = parseFloat(value) || 0; } else { obj[header] = value; } }); results.push(obj as T); } return results; } private getEvidenceTypes(interaction: ProteinInteraction): string[] { const types: string[] = []; if (interaction.nscore > 0) types.push('neighborhood'); if (interaction.fscore > 0) types.push('fusion'); if (interaction.pscore > 0) types.push('cooccurrence'); if (interaction.ascore > 0) types.push('coexpression'); if (interaction.escore > 0) types.push('experimental'); if (interaction.dscore > 0) types.push('database'); if (interaction.tscore > 0) types.push('textmining'); return types; } // Tool handlers private async handleGetProteinInteractions(args: any) { if (!isValidProteinArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid protein interaction arguments'); } try { const species = args.species || '9606'; const limit = args.limit || 10; const requiredScore = args.required_score || 400; const response = await this.apiClient.get('/tsv/interaction_partners', { params: { identifiers: args.protein_id, species: species, limit: limit, required_score: requiredScore, }, }); const interactions = this.parseTsvData<ProteinInteraction>(response.data); return { content: [ { type: 'text', text: JSON.stringify({ query_protein: args.protein_id, species: species, total_interactions: interactions.length, interactions: interactions.map(int => ({ partner_protein: int.preferredName_B, string_id: int.stringId_B, confidence_score: int.score, evidence_scores: { neighborhood: int.nscore, fusion: int.fscore, cooccurrence: int.pscore, coexpression: int.ascore, experimental: int.escore, database: int.dscore, textmining: int.tscore, } })) }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching protein interactions: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetInteractionNetwork(args: any) { if (!isValidNetworkArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid network arguments'); } try { const species = args.species || '9606'; const addNodes = args.add_nodes || 0; const requiredScore = args.required_score || 400; // Get network data const networkResponse = await this.apiClient.get('/tsv/network', { params: { identifiers: args.protein_ids.join('%0d'), species: species, add_white_nodes: addNodes, required_score: requiredScore, }, }); const interactions = this.parseTsvData<ProteinInteraction>(networkResponse.data); // Get node annotations const nodeResponse = await this.apiClient.get('/tsv/get_string_ids', { params: { identifiers: args.protein_ids.join('%0d'), species: species, }, }); const nodes = this.parseTsvData<NetworkNode>(nodeResponse.data); return { content: [ { type: 'text', text: JSON.stringify({ query_proteins: args.protein_ids, species: species, network_stats: { total_nodes: nodes.length, total_edges: interactions.length, average_degree: interactions.length > 0 ? (interactions.length * 2) / nodes.length : 0, }, nodes: nodes.map(node => ({ protein_name: node.preferredName, string_id: node.stringId, annotation: node.annotation, protein_size: node.protein_size, })), edges: interactions.map(int => ({ protein_a: int.preferredName_A, protein_b: int.preferredName_B, confidence_score: int.score, evidence_types: this.getEvidenceTypes(int), })) }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error building interaction network: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetFunctionalEnrichment(args: any) { if (!isValidEnrichmentArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid enrichment arguments'); } try { const species = args.species || '9606'; const params: any = { identifiers: args.protein_ids.join('%0d'), species: species, caller_identity: 'string-mcp-server', }; if (args.background_string_identifiers) { params.background_string_identifiers = args.background_string_identifiers.join('%0d'); } const response = await this.apiClient.get('/tsv/enrichment', { params }); const enrichments = this.parseTsvData<EnrichmentTerm>(response.data); // Group by category const groupedEnrichments: Record<string, EnrichmentTerm[]> = {}; enrichments.forEach(term => { if (!groupedEnrichments[term.category]) { groupedEnrichments[term.category] = []; } groupedEnrichments[term.category].push(term); }); return { content: [ { type: 'text', text: JSON.stringify({ query_proteins: args.protein_ids, species: species, total_terms: enrichments.length, enrichment_categories: Object.keys(groupedEnrichments), enrichments: groupedEnrichments, significant_terms: enrichments.filter(term => term.pvalue_fdr < 0.05).length, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error performing functional enrichment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetProteinAnnotations(args: any) { if (!isValidNetworkArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid protein annotation arguments'); } try { const species = args.species || '9606'; const response = await this.apiClient.get('/tsv/get_string_ids', { params: { identifiers: args.protein_ids.join('%0d'), species: species, }, }); const annotations = this.parseTsvData<ProteinAnnotation>(response.data); return { content: [ { type: 'text', text: JSON.stringify({ species: species, proteins: annotations.map(protein => ({ query_id: protein.preferredName, string_id: protein.stringId, preferred_name: protein.preferredName, ncbi_taxon_id: protein.ncbiTaxonId, annotation: protein.annotation, protein_size: protein.protein_size, })) }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error fetching protein annotations: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleFindHomologs(args: any) { if (!isValidHomologyArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid homology arguments'); } try { const species = args.species || '9606'; const params: any = { identifiers: args.protein_id, species: species, }; if (args.target_species) { params.target_species = args.target_species.join(','); } const response = await this.apiClient.get('/tsv/homology', { params }); const homologs = this.parseTsvData<HomologyResult>(response.data); // Group by species const groupedHomologs: Record<string, HomologyResult[]> = {}; homologs.forEach(homolog => { const speciesKey = `${homolog.ncbiTaxonId}_${homolog.taxonName}`; if (!groupedHomologs[speciesKey]) { groupedHomologs[speciesKey] = []; } groupedHomologs[speciesKey].push(homolog); }); return { content: [ { type: 'text', text: JSON.stringify({ query_protein: args.protein_id, source_species: species, total_homologs: homologs.length, species_count: Object.keys(groupedHomologs).length, homologs_by_species: groupedHomologs, }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error finding homologs: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleSearchProteins(args: any) { if (!isValidSearchArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid search arguments'); } try { const species = args.species || ''; const limit = args.limit || 10; const params: any = { identifiers: args.query, limit: limit, }; if (species) { params.species = species; } const response = await this.apiClient.get('/tsv/get_string_ids', { params }); const results = this.parseTsvData<ProteinAnnotation>(response.data); return { content: [ { type: 'text', text: JSON.stringify({ query: args.query, species_filter: species || 'all', total_results: results.length, proteins: results.map(protein => ({ string_id: protein.stringId, preferred_name: protein.preferredName, ncbi_taxon_id: protein.ncbiTaxonId, annotation: protein.annotation, protein_size: protein.protein_size, })) }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error searching proteins: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('STRING MCP server running on stdio'); } } const server = new StringServer(); server.run().catch(console.error);

Implementation Reference

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

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