Skip to main content
Glama

cBioPortal MCP Server

by pickleton89
genes.py12.1 kB
""" Genes endpoint module for the cBioPortal MCP server. Contains all gene-related endpoint methods: - search_genes: Search genes by keyword with pagination - get_genes: Get gene information using batch endpoint - get_multiple_genes: Fetch multiple genes concurrently - get_mutations_in_gene: Get mutations in a specific gene """ import asyncio import time from typing import Dict, List, Optional from ..api_client import APIClient from ..constants import FETCH_ALL_PAGE_SIZE from .base import handle_api_errors from ..utils.validation import ( validate_page_params, validate_sort_params, validate_keyword, ) from ..utils.logging import get_logger logger = get_logger(__name__) class GenesEndpoints: """Handles all gene-related endpoints for the cBioPortal MCP server.""" def __init__(self, api_client: APIClient, config=None): self.api_client = api_client self.config = config @handle_api_errors("search genes") async def search_genes( self, keyword: str, page_number: int = 0, page_size: int = 50, sort_by: Optional[str] = None, direction: str = "ASC", limit: Optional[int] = None, ) -> Dict: """ Search for genes by keyword in their symbol or name with pagination support. """ # Input Validation validate_keyword(keyword) validate_page_params(page_number, page_size, limit) validate_sort_params(sort_by, direction) try: api_call_params = { "keyword": keyword, "pageNumber": page_number, "pageSize": page_size, "direction": direction, } if sort_by: api_call_params["sortBy"] = sort_by if limit == 0: api_call_params["pageSize"] = FETCH_ALL_PAGE_SIZE genes_from_api = await self.api_client.make_api_request( "genes", params=api_call_params ) # Determine if the API might have more data api_might_have_more = len(genes_from_api) == api_call_params["pageSize"] # If 'fetch all' was intended and API returned less than max fetch size, then it's definitely the end. if ( api_call_params["pageSize"] == FETCH_ALL_PAGE_SIZE and len(genes_from_api) < FETCH_ALL_PAGE_SIZE ): api_might_have_more = False # Apply server-side limit if specified (after fetching the page from API) genes_for_response = genes_from_api if limit and limit > 0 and len(genes_from_api) > limit: genes_for_response = genes_from_api[:limit] # total_found in pagination now means number of items in this specific response payload # This makes the count consistent with the actual returned data structure total_items_in_response = len(genes_for_response) return { "genes": genes_for_response, "pagination": { "page": page_number, "page_size": page_size, # Report original requested page_size to client "total_found": total_items_in_response, "has_more": api_might_have_more, }, } except Exception as e: return {"error": f"Failed to search genes: {str(e)}"} @handle_api_errors("get genes") async def get_genes( self, gene_ids: List[str], gene_id_type: str = "ENTREZ_GENE_ID", projection: str = "SUMMARY", ) -> Dict: """ Get information about specific genes by their Hugo symbol or Entrez ID using batch endpoint. Args: gene_ids: List of gene IDs (Entrez IDs or Hugo symbols) gene_id_type: Type of gene ID provided (ENTREZ_GENE_ID or HUGO_GENE_SYMBOL) projection: Level of detail to return (ID, SUMMARY, DETAILED) Returns: Dictionary with gene information """ try: params = {"geneIdType": gene_id_type, "projection": projection} gene_data = await self.api_client.make_api_request( "genes/fetch", method="POST", params=params, json_data=gene_ids ) return {"genes": gene_data} except Exception as e: return {"error": f"Failed to get gene information: {str(e)}"} @handle_api_errors("get multiple genes") async def get_multiple_genes( self, gene_ids: List[str], gene_id_type: str = "ENTREZ_GENE_ID", projection: str = "SUMMARY", ) -> Dict: """ Get information about multiple genes concurrently. This method uses concurrency to fetch multiple genes in parallel, which is much more efficient than sequential requests for large batches. Args: gene_ids: List of gene IDs (Entrez IDs or Hugo symbols) gene_id_type: Type of gene ID provided (ENTREZ_GENE_ID or HUGO_GENE_SYMBOL) projection: Level of detail to return (ID, SUMMARY, DETAILED) Returns: Dictionary with gene information and performance metadata """ overall_start_time = time.time() # Single start time for the whole operation if not gene_ids: return { "genes": {}, "metadata": { "count": 0, "total_requested": 0, "errors": 0, "concurrent": True, "batches": 0, "execution_time": round(time.time() - overall_start_time, 3), }, } # For large gene lists, break into smaller batches for API compatibility batch_size = ( self.config.get("api.batch_size.genes", 100) if self.config else 100 # Default to 100 if no config provided ) gene_batches = [ gene_ids[i : i + batch_size] for i in range(0, len(gene_ids), batch_size) ] async def fetch_gene_batch(batch): try: params = {"geneIdType": gene_id_type, "projection": projection} batch_data = await self.api_client.make_api_request( "genes/fetch", method="POST", params=params, json_data=batch ) return {"data": batch_data, "success": True} except Exception as e: return {"error": str(e), "success": False} # Create tasks for all batches and run them concurrently tasks = [fetch_gene_batch(batch) for batch in gene_batches] batch_results = await asyncio.gather(*tasks) # Process results all_genes = [] error_count = 0 for result in batch_results: if result["success"]: all_genes.extend(result["data"]) else: error_count += 1 # Convert to dictionary for easier lookup genes_dict = {} key_field = ( "hugoGeneSymbol" if gene_id_type == "HUGO_GENE_SYMBOL" else "entrezGeneId" ) for gene in all_genes: gene_key_value = gene.get(key_field) if gene_key_value: genes_dict[str(gene_key_value)] = gene return { "genes": genes_dict, "metadata": { "count": len(genes_dict), "total_requested": len(gene_ids), "errors": error_count, "execution_time": round(time.time() - overall_start_time, 3), "concurrent": True, "batches": len(gene_batches), }, } @handle_api_errors("get mutations in gene") async def get_mutations_in_gene( self, gene_id: str, study_id: str, sample_list_id: str, page_number: int = 0, page_size: int = 50, sort_by: Optional[str] = None, direction: str = "ASC", limit: Optional[int] = None, ) -> Dict: """ Get mutations in a specific gene for a given study and sample list, with pagination support. Uses the /molecular-profiles/{molecularProfileId}/mutations endpoint with GET and query parameters. The molecularProfileId is dynamically determined based on the studyId. """ try: molecular_profiles_response = await self.api_client.make_api_request( f"studies/{study_id}/molecular-profiles" ) if ( isinstance(molecular_profiles_response, dict) and "api_error" in molecular_profiles_response ): return { "error": f"Failed to fetch molecular profiles for study {study_id} to find mutation profile", "details": molecular_profiles_response, } mutation_profile_id = None if isinstance(molecular_profiles_response, list): for profile in molecular_profiles_response: if profile.get("molecularAlterationType") == "MUTATION_EXTENDED": mutation_profile_id = profile.get("molecularProfileId") break if not mutation_profile_id: return { "error": f"No MUTATION_EXTENDED molecular profile found for study {study_id}" } api_call_params = { "studyId": study_id, "sampleListId": sample_list_id, "pageNumber": page_number, "pageSize": page_size, "direction": direction, } if sort_by: api_call_params["sortBy"] = sort_by if limit == 0: api_call_params["pageSize"] = FETCH_ALL_PAGE_SIZE if str(gene_id).isdigit(): api_call_params["entrezGeneId"] = gene_id else: api_call_params["hugoGeneSymbol"] = gene_id endpoint = f"molecular-profiles/{mutation_profile_id}/mutations" mutations_from_api = await self.api_client.make_api_request( endpoint, method="GET", params=api_call_params ) if ( isinstance(mutations_from_api, dict) and "api_error" in mutations_from_api ): return { "error": "API error fetching mutations", "details": mutations_from_api, "request_params": api_call_params, } if not isinstance(mutations_from_api, list): return { "error": "Unexpected API response type for mutations (expected list)", "details": mutations_from_api, "request_params": api_call_params, } api_might_have_more = len(mutations_from_api) == api_call_params["pageSize"] if ( api_call_params["pageSize"] == FETCH_ALL_PAGE_SIZE and len(mutations_from_api) < FETCH_ALL_PAGE_SIZE ): api_might_have_more = False mutations_for_response = mutations_from_api if limit and limit > 0 and len(mutations_from_api) > limit: mutations_for_response = mutations_from_api[:limit] total_items_in_response = len(mutations_for_response) return { "mutations": mutations_for_response, "pagination": { "page": page_number, "page_size": page_size, "total_found": total_items_in_response, "has_more": api_might_have_more, }, } except Exception as e: return { "error": f"An unexpected error occurred in get_mutations_in_gene: {str(e)}" }

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/pickleton89/cbioportal-mcp'

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