Skip to main content
Glama

OpenTargets MCP Server

index.tsโ€ข19.8 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'; // Type guards and validation functions const isValidTargetSearchArgs = (args: any): args is { query: string; size?: number; format?: string } => { return ( typeof args === 'object' && args !== null && typeof args.query === 'string' && args.query.length > 0 && (args.size === undefined || (typeof args.size === 'number' && args.size > 0 && args.size <= 500)) && (args.format === undefined || ['json', 'tsv'].includes(args.format)) ); }; const isValidDiseaseSearchArgs = (args: any): args is { query: string; size?: number; format?: string } => { return ( typeof args === 'object' && args !== null && typeof args.query === 'string' && args.query.length > 0 && (args.size === undefined || (typeof args.size === 'number' && args.size > 0 && args.size <= 500)) && (args.format === undefined || ['json', 'tsv'].includes(args.format)) ); }; const isValidAssociationArgs = (args: any): args is { targetId?: string; diseaseId?: string; minScore?: number; size?: number } => { return ( typeof args === 'object' && args !== null && (args.targetId === undefined || typeof args.targetId === 'string') && (args.diseaseId === undefined || typeof args.diseaseId === 'string') && (args.minScore === undefined || (typeof args.minScore === 'number' && args.minScore >= 0 && args.minScore <= 1)) && (args.size === undefined || (typeof args.size === 'number' && args.size > 0 && args.size <= 500)) && (args.targetId !== undefined || args.diseaseId !== undefined) ); }; const isValidIdArgs = (args: any): args is { id: string } => { return ( typeof args === 'object' && args !== null && typeof args.id === 'string' && args.id.length > 0 ); }; class OpenTargetsServer { private server: Server; private apiClient: AxiosInstance; private graphqlClient: AxiosInstance; constructor() { this.server = new Server( { name: 'opentargets-server', version: '0.1.0', }, { capabilities: { resources: {}, tools: {}, }, } ); // Initialize Open Targets REST API client this.apiClient = axios.create({ baseURL: 'https://api.platform.opentargets.org/api/v4', timeout: 30000, headers: { 'User-Agent': 'OpenTargets-MCP-Server/0.1.0', 'Content-Type': 'application/json', }, }); // Initialize Open Targets GraphQL API client this.graphqlClient = axios.create({ baseURL: 'https://api.platform.opentargets.org/api/v4/graphql', timeout: 30000, headers: { 'User-Agent': 'OpenTargets-MCP-Server/0.1.0', 'Content-Type': 'application/json', }, }); this.setupResourceHandlers(); this.setupToolHandlers(); // Error handling this.server.onerror = (error: Error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupResourceHandlers() { this.server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [ { uriTemplate: 'opentargets://target/{id}', name: 'Open Targets target information', mimeType: 'application/json', description: 'Complete target information for an Ensembl gene ID', }, { uriTemplate: 'opentargets://disease/{id}', name: 'Open Targets disease information', mimeType: 'application/json', description: 'Complete disease information for an EFO ID', }, { uriTemplate: 'opentargets://drug/{id}', name: 'Open Targets drug information', mimeType: 'application/json', description: 'Complete drug information for a ChEMBL ID', }, { uriTemplate: 'opentargets://association/{targetId}/{diseaseId}', name: 'Target-disease association', mimeType: 'application/json', description: 'Target-disease association evidence and scoring', }, { uriTemplate: 'opentargets://search/{query}', name: 'Search results', mimeType: 'application/json', description: 'Search results across targets, diseases, and drugs', }, ], }) ); this.server.setRequestHandler( ReadResourceRequestSchema, async (request: any) => { const uri = request.params.uri; // Handle target info requests const targetMatch = uri.match(/^opentargets:\/\/target\/([A-Z0-9_]+)$/); if (targetMatch) { const targetId = targetMatch[1]; try { const response = await this.apiClient.get(`/target/${targetId}`); 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 target ${targetId}: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } // Handle disease info requests const diseaseMatch = uri.match(/^opentargets:\/\/disease\/([A-Z0-9_]+)$/); if (diseaseMatch) { const diseaseId = diseaseMatch[1]; try { const response = await this.apiClient.get(`/disease/${diseaseId}`); 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 disease ${diseaseId}: ${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_targets', description: 'Search for therapeutic targets by gene symbol, name, or description', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (gene symbol, name, description)' }, size: { type: 'number', description: 'Number of results to return (1-500, default: 25)', minimum: 1, maximum: 500 }, format: { type: 'string', enum: ['json', 'tsv'], description: 'Output format (default: json)' }, }, required: ['query'], }, }, { name: 'search_diseases', description: 'Search for diseases by name, synonym, or description', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (disease name, synonym, description)' }, size: { type: 'number', description: 'Number of results to return (1-500, default: 25)', minimum: 1, maximum: 500 }, format: { type: 'string', enum: ['json', 'tsv'], description: 'Output format (default: json)' }, }, required: ['query'], }, }, { name: 'get_target_disease_associations', description: 'Get target-disease associations with evidence scores', inputSchema: { type: 'object', properties: { targetId: { type: 'string', description: 'Target Ensembl gene ID' }, diseaseId: { type: 'string', description: 'Disease EFO ID' }, minScore: { type: 'number', description: 'Minimum association score (0-1)', minimum: 0, maximum: 1 }, size: { type: 'number', description: 'Number of results to return (1-500, default: 25)', minimum: 1, maximum: 500 }, }, required: [], }, }, { name: 'get_disease_targets_summary', description: 'Get overview of all targets associated with a disease', inputSchema: { type: 'object', properties: { diseaseId: { type: 'string', description: 'Disease EFO ID' }, minScore: { type: 'number', description: 'Minimum association score (0-1)', minimum: 0, maximum: 1 }, size: { type: 'number', description: 'Number of targets to return (1-500, default: 50)', minimum: 1, maximum: 500 }, }, required: ['diseaseId'], }, }, { name: 'get_target_details', description: 'Get comprehensive target information', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Target Ensembl gene ID' }, }, required: ['id'], }, }, { name: 'get_disease_details', description: 'Get comprehensive disease information', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Disease EFO ID' }, }, required: ['id'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => { const { name, arguments: args } = request.params; switch (name) { case 'search_targets': return this.handleSearchTargets(args); case 'search_diseases': return this.handleSearchDiseases(args); case 'get_target_disease_associations': return this.handleGetTargetDiseaseAssociations(args); case 'get_disease_targets_summary': return this.handleGetDiseaseTargetsSummary(args); case 'get_target_details': return this.handleGetTargetDetails(args); case 'get_disease_details': return this.handleGetDiseaseDetails(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } }); } private async handleSearchTargets(args: any) { if (!isValidTargetSearchArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid target search arguments'); } try { const query = ` query SearchTargets($queryString: String!) { search(queryString: $queryString, entityNames: ["target"]) { hits { id name description entity } } } `; const response = await this.graphqlClient.post('', { query, variables: { queryString: args.query } }); // Limit results on client side const hits = response.data.data?.search?.hits || []; const limitedHits = hits.slice(0, args.size || 25); const result = { ...response.data, data: { search: { hits: limitedHits, total: hits.length } } }; return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error searching targets: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleSearchDiseases(args: any) { if (!isValidDiseaseSearchArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid disease search arguments'); } try { const query = ` query SearchDiseases($queryString: String!) { search(queryString: $queryString, entityNames: ["disease"]) { hits { id name description entity } } } `; const response = await this.graphqlClient.post('', { query, variables: { queryString: args.query } }); // Limit results on client side const hits = response.data.data?.search?.hits || []; const limitedHits = hits.slice(0, args.size || 25); const result = { ...response.data, data: { search: { hits: limitedHits, total: hits.length } } }; return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error searching diseases: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetTargetDiseaseAssociations(args: any) { if (!isValidAssociationArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid association arguments'); } try { // If only targetId provided, get associations for that target if (args.targetId && !args.diseaseId) { const query = `query GetTargetAssociations($ensemblId: String!) { target(ensemblId: $ensemblId) { id approvedSymbol associatedDiseases { rows { disease { id name } score } } } }`; const response = await this.graphqlClient.post('', { query, variables: { ensemblId: args.targetId, size: args.size || 25 } }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } // If only diseaseId provided, get associations for that disease else if (args.diseaseId && !args.targetId) { const query = `query GetDiseaseAssociations($efoId: String!) { disease(efoId: $efoId) { id name associatedTargets { rows { target { id approvedSymbol approvedName } score } } } }`; const response = await this.graphqlClient.post('', { query, variables: { efoId: args.diseaseId, size: args.size || 25 } }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } // If both provided, return the association between them else { return { content: [ { type: 'text', text: JSON.stringify({ message: "Specific target-disease pair association lookup not yet implemented", suggestion: "Use targetId OR diseaseId to get associations for that entity" }, null, 2), }, ], }; } } catch (error) { return { content: [ { type: 'text', text: `Error getting associations: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetDiseaseTargetsSummary(args: any) { if (!isValidIdArgs(args) && !args.diseaseId) { throw new McpError(ErrorCode.InvalidParams, 'Disease ID is required'); } try { const diseaseId = args.diseaseId || args.id; const query = `query GetDiseaseTargetsSummary($efoId: String!) { disease(efoId: $efoId) { id name associatedTargets { count rows { target { id approvedSymbol approvedName } score } } } }`; const response = await this.graphqlClient.post('', { query, variables: { efoId: diseaseId, size: args.size || 50 } }); const diseaseData = response.data.data?.disease; const associations = diseaseData?.associatedTargets; const summary = { diseaseId, diseaseName: diseaseData?.name, totalTargets: associations?.count || 0, topTargets: associations?.rows?.slice(0, 10).map((assoc: any) => ({ targetId: assoc.target.id, targetSymbol: assoc.target.approvedSymbol, targetName: assoc.target.approvedName, associationScore: assoc.score, datatypeScores: assoc.datatypeScores, })) || [], fullResults: response.data, }; return { content: [ { type: 'text', text: JSON.stringify(summary, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error getting disease targets summary: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetTargetDetails(args: any) { if (!isValidIdArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Target ID is required'); } try { const query = `query GetTarget($ensemblId: String!) { target(ensemblId: $ensemblId) { id approvedName approvedSymbol biotype } }`; const response = await this.graphqlClient.post('', { query, variables: { ensemblId: args.id } }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error getting target details: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } private async handleGetDiseaseDetails(args: any) { if (!isValidIdArgs(args)) { throw new McpError(ErrorCode.InvalidParams, 'Disease ID is required'); } try { const query = `query GetDisease($efoId: String!) { disease(efoId: $efoId) { id name description } }`; const response = await this.graphqlClient.post('', { query, variables: { efoId: args.id } }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error getting disease details: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Open Targets MCP server running on stdio'); } } const server = new OpenTargetsServer(); 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/OpenTargets-MCP-Server'

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