@iflow-mcp/go-server
Version:
A comprehensive Model Context Protocol server for accessing Gene Ontology data with tools for term search, ontology navigation, gene annotations, and enrichment analysis
626 lines (578 loc) • 19 kB
text/typescript
/**
* Gene Ontology MCP Server
* Production-ready Model Context Protocol server for Gene Ontology data access
*
* Copyright (c) 2025 Augmented Nature
* Licensed under MIT License - see LICENSE file for details
*
* Developed by Augmented Nature - https://augmentednature.ai
* Advancing AI for Scientific Discovery
*
* This server provides comprehensive access to Gene Ontology (GO) data through the
* Model Context Protocol, enabling AI systems to perform ontology-based analysis,
* gene annotation research, and functional enrichment studies.
*/
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';
// Gene Ontology API interfaces
interface GOTerm {
id: string;
name: string;
definition: string;
namespace: string;
obsolete?: boolean;
replaced_by?: string[];
consider?: string[];
}
interface GOAnnotation {
id: string;
gene_product_id: string;
gene_product_symbol: string;
qualifier?: string;
go_id: string;
go_name: string;
evidence_code: string;
reference: string;
taxon_id: string;
date: string;
}
interface EnrichmentResult {
term_id: string;
term_name: string;
p_value: number;
adjusted_p_value?: number;
gene_count: number;
background_count: number;
genes: string[];
}
// Type guards and validation functions
const isValidSearchArgs = (args: any): args is {
query: string;
ontology?: string;
size?: number;
exact?: boolean;
include_obsolete?: boolean;
} => {
return (
typeof args === 'object' &&
args !== null &&
typeof args.query === 'string' &&
args.query.length > 0 &&
(args.ontology === undefined || ['molecular_function', 'biological_process', 'cellular_component', 'all'].includes(args.ontology)) &&
(args.size === undefined || (typeof args.size === 'number' && args.size > 0 && args.size <= 500)) &&
(args.exact === undefined || typeof args.exact === 'boolean') &&
(args.include_obsolete === undefined || typeof args.include_obsolete === 'boolean')
);
};
const isValidTermArgs = (args: any): args is { id: string } => {
return (
typeof args === 'object' &&
args !== null &&
typeof args.id === 'string' &&
args.id.length > 0
);
};
const isValidGeneArgs = (args: any): args is {
gene: string;
species?: string;
ontology?: string;
evidence?: string;
} => {
return (
typeof args === 'object' &&
args !== null &&
typeof args.gene === 'string' &&
args.gene.length > 0 &&
(args.species === undefined || typeof args.species === 'string') &&
(args.ontology === undefined || ['molecular_function', 'biological_process', 'cellular_component', 'all'].includes(args.ontology)) &&
(args.evidence === undefined || typeof args.evidence === 'string')
);
};
const isValidAnnotationSearchArgs = (args: any): args is {
go_id: string;
species?: string;
evidence?: string;
size?: number;
} => {
return (
typeof args === 'object' &&
args !== null &&
typeof args.go_id === 'string' &&
args.go_id.length > 0 &&
(args.species === undefined || typeof args.species === 'string') &&
(args.evidence === undefined || typeof args.evidence === 'string') &&
(args.size === undefined || (typeof args.size === 'number' && args.size > 0 && args.size <= 1000))
);
};
const isValidEnrichmentArgs = (args: any): args is {
genes: string[];
species?: string;
ontology?: string;
background?: string[];
p_value_threshold?: number;
} => {
return (
typeof args === 'object' &&
args !== null &&
Array.isArray(args.genes) &&
args.genes.length > 0 &&
args.genes.every((gene: any) => typeof gene === 'string') &&
(args.species === undefined || typeof args.species === 'string') &&
(args.ontology === undefined || ['molecular_function', 'biological_process', 'cellular_component', 'all'].includes(args.ontology)) &&
(args.background === undefined || (Array.isArray(args.background) && args.background.every((gene: any) => typeof gene === 'string'))) &&
(args.p_value_threshold === undefined || (typeof args.p_value_threshold === 'number' && args.p_value_threshold > 0 && args.p_value_threshold <= 1))
);
};
const isValidCompareTermsArgs = (args: any): args is {
term1: string;
term2: string;
method?: string;
} => {
return (
typeof args === 'object' &&
args !== null &&
typeof args.term1 === 'string' &&
args.term1.length > 0 &&
typeof args.term2 === 'string' &&
args.term2.length > 0 &&
(args.method === undefined || ['resnik', 'lin', 'jiang_conrath', 'semantic'].includes(args.method))
);
};
class GeneOntologyServer {
private server: Server;
private goApiClient: AxiosInstance;
private quickGoClient: AxiosInstance;
constructor() {
this.server = new Server(
{
name: 'go-server',
version: '1.0.0',
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Initialize GO API clients
this.goApiClient = axios.create({
baseURL: 'https://api.geneontology.org',
timeout: 30000,
headers: {
'User-Agent': 'GO-MCP-Server/1.0.0',
'Accept': 'application/json',
},
});
this.quickGoClient = axios.create({
baseURL: 'https://www.ebi.ac.uk/QuickGO/services',
timeout: 30000,
headers: {
'User-Agent': 'GO-MCP-Server/1.0.0',
'Accept': '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() {
this.server.setRequestHandler(
ListResourceTemplatesRequestSchema,
async () => ({
resourceTemplates: [
{
uriTemplate: 'go://term/{id}',
name: 'Gene Ontology term information',
mimeType: 'application/json',
description: 'Complete GO term information including definition, relationships, and metadata',
},
{
uriTemplate: 'go://annotations/{gene}',
name: 'Gene annotations',
mimeType: 'application/json',
description: 'GO annotations for a specific gene or protein',
},
{
uriTemplate: 'go://search/{query}',
name: 'GO search results',
mimeType: 'application/json',
description: 'Search results across GO terms and annotations',
},
{
uriTemplate: 'go://hierarchy/{id}',
name: 'GO term hierarchy',
mimeType: 'application/json',
description: 'Hierarchical relationships for a GO term',
},
],
})
);
this.server.setRequestHandler(
ReadResourceRequestSchema,
async (request: any) => {
const uri = request.params.uri;
// Handle GO term requests
const termMatch = uri.match(/^go:\/\/term\/(GO:\d{7})$/);
if (termMatch) {
const termId = termMatch[1];
try {
const response = await this.quickGoClient.get(`/ontology/go/terms/${termId}`);
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 GO term ${termId}: ${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_go_terms',
description: 'Search across Gene Ontology terms by keyword, name, or definition',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query (term name, keyword, or definition)' },
ontology: {
type: 'string',
enum: ['molecular_function', 'biological_process', 'cellular_component', 'all'],
description: 'GO ontology to search (default: all)'
},
size: { type: 'number', description: 'Number of results to return (1-500, default: 25)', minimum: 1, maximum: 500 },
exact: { type: 'boolean', description: 'Exact match only (default: false)' },
include_obsolete: { type: 'boolean', description: 'Include obsolete terms (default: false)' },
},
required: ['query'],
},
},
{
name: 'get_go_term',
description: 'Get detailed information for a specific GO term',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'GO term identifier (e.g., GO:0008150)' },
},
required: ['id'],
},
},
{
name: 'validate_go_id',
description: 'Validate GO identifier format and check if term exists',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'GO identifier to validate' },
},
required: ['id'],
},
},
{
name: 'get_ontology_stats',
description: 'Get statistics about GO ontologies (term counts, recent updates)',
inputSchema: {
type: 'object',
properties: {
ontology: { type: 'string', description: 'Specific ontology or "all" for overall stats' },
},
required: [],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'search_go_terms':
return this.handleSearchGoTerms(args);
case 'get_go_term':
return this.handleGetGoTerm(args);
case 'validate_go_id':
return this.handleValidateGoId(args);
case 'get_ontology_stats':
return this.handleGetOntologyStats(args);
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
});
}
// Helper function to validate and normalize GO IDs
private normalizeGoId(id: string): string {
if (id.startsWith('GO:')) {
return id;
}
if (/^\d{7}$/.test(id)) {
return `GO:${id}`;
}
return id;
}
// Tool implementations
private async handleSearchGoTerms(args: any) {
if (!isValidSearchArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'Invalid search arguments');
}
try {
const params: any = {
query: args.query,
limit: args.size || 25,
page: 1,
};
if (args.ontology && args.ontology !== 'all') {
params.aspect = args.ontology === 'molecular_function' ? 'F' :
args.ontology === 'biological_process' ? 'P' : 'C';
}
if (args.include_obsolete === false) {
params.obsolete = 'false';
}
const response = await this.quickGoClient.get('/ontology/go/search', { params });
const searchResults = {
query: args.query,
totalResults: response.data.numberOfHits || 0,
returnedResults: response.data.results?.length || 0,
results: response.data.results?.map((term: any) => ({
id: term.id,
name: term.name,
definition: term.definition?.text || 'No definition available',
namespace: term.aspect === 'F' ? 'molecular_function' :
term.aspect === 'P' ? 'biological_process' : 'cellular_component',
obsolete: term.isObsolete || false,
url: `https://www.ebi.ac.uk/QuickGO/term/${term.id}`
})) || []
};
return {
content: [
{
type: 'text',
text: JSON.stringify(searchResults, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error searching GO terms: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
}
private async handleGetGoTerm(args: any) {
if (!isValidTermArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'GO term ID is required');
}
try {
const termId = this.normalizeGoId(args.id);
const response = await this.quickGoClient.get(`/ontology/go/terms/${termId}`);
const termInfo = response.data.results?.[0];
if (!termInfo) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `GO term not found: ${termId}`,
suggestion: 'Check the GO ID format (e.g., GO:0008150) or search for the term first'
}, null, 2),
},
],
isError: true,
};
}
const detailedTerm = {
id: termInfo.id,
name: termInfo.name,
definition: {
text: termInfo.definition?.text || 'No definition available',
references: termInfo.definition?.xrefs || []
},
namespace: termInfo.aspect === 'F' ? 'molecular_function' :
termInfo.aspect === 'P' ? 'biological_process' : 'cellular_component',
obsolete: termInfo.isObsolete || false,
replaced_by: termInfo.replacedBy || [],
consider: termInfo.consider || [],
synonyms: termInfo.synonyms || [],
xrefs: termInfo.xrefs || [],
url: `https://www.ebi.ac.uk/QuickGO/term/${termInfo.id}`,
amigo_url: `http://amigo.geneontology.org/amigo/term/${termInfo.id}`
};
return {
content: [
{
type: 'text',
text: JSON.stringify(detailedTerm, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error fetching GO term: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
}
private async handleValidateGoId(args: any) {
if (!isValidTermArgs(args)) {
throw new McpError(ErrorCode.InvalidParams, 'GO ID is required');
}
try {
const termId = this.normalizeGoId(args.id);
// Check format
const isValidFormat = /^GO:\d{7}$/.test(termId);
let exists = false;
let termInfo = null;
if (isValidFormat) {
try {
const response = await this.quickGoClient.get(`/ontology/go/terms/${termId}`);
termInfo = response.data.results?.[0];
exists = !!termInfo;
} catch (e) {
exists = false;
}
}
const validation = {
input_id: args.id,
normalized_id: termId,
valid_format: isValidFormat,
exists: exists,
term_info: exists ? {
name: termInfo?.name,
namespace: termInfo?.aspect === 'F' ? 'molecular_function' :
termInfo?.aspect === 'P' ? 'biological_process' : 'cellular_component',
obsolete: termInfo?.isObsolete || false
} : null,
format_rules: {
pattern: 'GO:NNNNNNN',
example: 'GO:0008150',
description: 'GO identifiers consist of "GO:" followed by exactly 7 digits'
}
};
return {
content: [
{
type: 'text',
text: JSON.stringify(validation, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error validating GO ID: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
}
private async handleGetOntologyStats(args: any) {
try {
const stats = {
ontology: args.ontology || 'all',
last_updated: new Date().toISOString().split('T')[0],
note: 'Statistics are approximate and may vary based on data access methods',
sources: {
quickgo: 'https://www.ebi.ac.uk/QuickGO/',
go_consortium: 'https://geneontology.org/',
amigo: 'http://amigo.geneontology.org/'
},
approximate_counts: {
molecular_function: {
description: 'Molecular activities of gene products',
estimated_terms: '~11,000',
root_term: 'GO:0003674'
},
biological_process: {
description: 'Larger processes accomplished by multiple molecular activities',
estimated_terms: '~30,000',
root_term: 'GO:0008150'
},
cellular_component: {
description: 'Locations relative to cellular structures',
estimated_terms: '~4,000',
root_term: 'GO:0005575'
}
},
evidence_codes: {
experimental: ['EXP', 'IDA', 'IPI', 'IMP', 'IGI', 'IEP'],
high_throughput: ['HTP', 'HDA', 'HMP', 'HGI', 'HEP'],
computational: ['IBA', 'IBD', 'IKR', 'IRD', 'ISS', 'ISO', 'ISA', 'ISM', 'IGC', 'RCA'],
author_statement: ['TAS', 'NAS'],
curator_statement: ['IC', 'ND'],
electronic: ['IEA']
}
};
return {
content: [
{
type: 'text',
text: JSON.stringify(stats, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error getting ontology stats: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
isError: true,
};
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Gene Ontology MCP server running on stdio');
}
}
const server = new GeneOntologyServer();
server.run().catch(console.error);