From 1d8b2835256070ddb593ba25da096925898ae6e6 Mon Sep 17 00:00:00 2001 From: Hernan Date: Fri, 24 Jul 2026 11:32:49 -0300 Subject: [PATCH 1/2] Migrate Modulector integration to SDK Replaced direct Modulector HTTP calls with modulector-sdk wrappers in `MRNAService` and updated all related consumers (`api_service`, `biomarkers`, `molecules_details`, and assistant tools). Added dedicated SDK-backed methods for miRNA/methylation lookup, interactions, diseases, drugs, and code/site search, plus paginated response normalization and safer fallbacks. Also introduced `MODULECTOR_BASE_URL` settings, added `modulector-sdk` dependency, updated MCP server command args for BioMCP, and fixed a null-response edge case in biomarker gene lookup. --- config/mcp_servers.json | 4 +- config/requirements.txt | 3 +- src/api_service/mongo_service.py | 7 +- src/api_service/mrna_service.py | 209 ++++++++++++++++++------ src/api_service/views.py | 52 +++--- src/assistant/services/tools.py | 133 ++++++++++++++- src/biomarkers/views.py | 27 +-- src/molecules_details/views.py | 9 +- src/multiomics_intermediate/settings.py | 14 +- 9 files changed, 326 insertions(+), 132 deletions(-) diff --git a/config/mcp_servers.json b/config/mcp_servers.json index e79f2593..53313b39 100644 --- a/config/mcp_servers.json +++ b/config/mcp_servers.json @@ -5,8 +5,8 @@ "enabled": true, "description": "Biomedical research: PubMed papers, bioRxiv, ClinicalTrials.gov, NCI, variants, OncoKB", "transport": "stdio", - "command": "biomcp", - "args": ["run", "--mode", "stdio"], + "command": "biomcp-cli", + "args": ["mcp"], "env": {} } } diff --git a/config/requirements.txt b/config/requirements.txt index 0cc208d0..1a3603de 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -38,4 +38,5 @@ langchain-mcp-adapters==0.2.2 pgvector==0.4.2 sentence-transformers==3.0.0 openai==2.38.0 -biomcp-cli==0.8.22 \ No newline at end of file +biomcp-cli==0.8.22 +modulector-sdk==2.4.0 \ No newline at end of file diff --git a/src/api_service/mongo_service.py b/src/api_service/mongo_service.py index 02cc2b13..cb8ac047 100644 --- a/src/api_service/mongo_service.py +++ b/src/api_service/mongo_service.py @@ -222,12 +222,7 @@ def __get_standard_ids(file_type: FileType, molecules: List[str]) -> Optional[Di method='post' ) elif file_type == FileType.MIRNA: - data = global_mrna_service.get_modulector_service_content( - 'mirna-codes', - request_params={'mirna_codes': molecules}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_mirna_codes(mirna_codes=molecules) else: # In case of methylation, cBioPortal don't manage the methylation sites, so we don't need to use Modulector. # Generates a dummy dict with the same keys and values as the molecules list diff --git a/src/api_service/mrna_service.py b/src/api_service/mrna_service.py index d431f53e..04ec4910 100644 --- a/src/api_service/mrna_service.py +++ b/src/api_service/mrna_service.py @@ -1,35 +1,36 @@ import logging from json.decoder import JSONDecodeError -from typing import Any, Dict, Optional, Literal, Union +from typing import Any, Dict, List, Optional, Literal, Union import requests from django.conf import settings from django.http import QueryDict from requests.exceptions import ConnectionError +import modulector_sdk as modulector +from modulector_sdk import PaginatedResponse + class MRNAService(object): - url_modulector_prefix: str url_bioapi_prefix: str + _modulector_base_url: str def __init__(self): - modulector_settings = settings.MODULECTOR_SETTINGS - self.url_modulector_prefix = self.__build_url(modulector_settings) - bioapi_settings = settings.BIOAPI_SETTINGS self.url_bioapi_prefix = self.__build_url(bioapi_settings) + self._modulector_base_url = settings.MODULECTOR_BASE_URL @staticmethod - def __build_url(settings: Dict[str, Any]) -> str: + def __build_url(svc_settings: Dict[str, Any]) -> str: """ Constructs the URL based on the settings provided. If the port is the default for the protocol (80 for http, 443 for https), it is omitted. Otherwise, the port is included in the URL. - @param settings: Dictionary containing protocol, host, and port information. + @param svc_settings: Dictionary containing protocol, host, and port information. @return: Constructed URL as a string. """ - protocol = settings['protocol'] - host = settings['host'] - port = settings['port'] + protocol = svc_settings['protocol'] + host = svc_settings['host'] + port = svc_settings['port'] if (protocol == 'http' and port == 80) or (protocol == 'https' and port == 443): return f"{protocol}://{host}" @@ -41,43 +42,35 @@ def __generate_rest_query_params(get_request: QueryDict) -> str: """ Generates a string with all the query params from GET request. @param get_request: GET request with query params to send to DRF backend - @return: String to send to Modulector/BioAPI APIs + @return: String to send to BioAPI """ return '&'.join([f'{key}={value}' for (key, value) in get_request.items()]) - def __get_service_content( + def __get_bioapi_content( self, service_name: str, request_params: QueryDict, is_paginated: bool, - url_prefix: str, method: Literal['get', 'post'], - append_slash: bool ) -> Optional[Union[Dict, str]]: """ - Generic function to make a request to a Modulector/BioAPI service - @param service_name: Modulector/BioAPI service to consume - @param request_params: GET/POST request with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param url_prefix: URL of the Modulector or BioAPI service + Generic function to make a request to a BioAPI service. + @param service_name: BioAPI service to consume + @param request_params: GET/POST request with query params + @param is_paginated: True if the expected response is paginated @param method: Request method (GET or POST) - @param append_slash: If True appends a slash to prevent issues with Django. - @return: JSON data retrieved from the Modulector service. None if response has 404 status code + @return: JSON data retrieved from BioAPI. None if response has 404 status code """ - url = f'{url_prefix}/{service_name}' + url = f'{self.url_bioapi_prefix}/{service_name}/' data = None # Prevents Mypy warning try: if method == 'get': params = self.__generate_rest_query_params(request_params) if params: - url += f'/?{params}' + url += f'/?{params}/' data = requests.get(url) else: - # Prevents issues with Django APPEND_SLASH option - if append_slash and not url.endswith('/'): - url += '/' - data = requests.post(url, json=request_params) if data.status_code != 200: @@ -94,7 +87,7 @@ def __get_service_content( return None except (ConnectionError, JSONDecodeError) as ex: - logging.error(f'Received data from Modulector/BioAPI: {data}') + logging.error(f'Received data from BioAPI: {data}') logging.exception(ex) if is_paginated: @@ -106,24 +99,6 @@ def __get_service_content( } return None - def get_modulector_service_content( - self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - method: Literal['get', 'post'] = 'get' - ) -> Optional[Dict]: - """ - Makes a request to a Modulector service. - @param service_name: Modulector service to consume - @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param method: Request method (GET or POST) - @return: JSON data retrieved from the Modulector service. None if response has 404 status code - """ - return self.__get_service_content(service_name, request_params, is_paginated, self.url_modulector_prefix, - method, append_slash=True) - def get_bioapi_service_content( self, service_name: str, @@ -135,12 +110,144 @@ def get_bioapi_service_content( Makes a request to a BioAPI service. @param service_name: BioAPI service to consume @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error + @param is_paginated: True if the expected response is paginated @param method: Request method (GET or POST) - @return: JSON data retrieved from the Modulector service. None if response has 404 status code + @return: JSON data retrieved from BioAPI. None if response has 404 status code """ - return self.__get_service_content(service_name, request_params, is_paginated, self.url_bioapi_prefix, method, - append_slash=False) + return self.__get_bioapi_content(service_name, request_params, is_paginated, method) + + # ------------------------------------------------------------------ # + # Modulector SDK wrappers # + # ------------------------------------------------------------------ # + + def _paginated_to_dict(self, result: Optional[PaginatedResponse]) -> Dict: + """Converts a SDK PaginatedResponse to the dict format expected by the frontend.""" + if result is None: + return {'count': 0, 'next': '', 'previous': '', 'results': []} + return { + 'count': result.count, + 'next': result.next or '', + 'previous': result.previous or '', + 'results': list(result.results or []), + } + + def get_mirna_details(self, mirna: str) -> Optional[Dict]: + """Get miRNA details from Modulector SDK.""" + try: + return modulector.get_mirna_details(mirna=mirna, base_url=self._modulector_base_url) + except Exception as ex: + logging.exception(ex) + return None + + def get_methylation_details(self, methylation_site: str) -> Optional[Dict]: + """Get methylation site details from Modulector SDK.""" + try: + return modulector.get_methylation_details( + methylation_site=methylation_site, + base_url=self._modulector_base_url + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_mirna_target_interactions( + self, + mirna: Optional[str] = None, + gene: Optional[str] = None, + score: Optional[str] = None, + include_pubmeds: bool = False, + page: Optional[int] = None, + page_size: Optional[int] = None, + ) -> Dict: + """Get miRNA-target interactions from Modulector SDK.""" + try: + result = modulector.get_mirna_target_interactions( + mirna=mirna, + gene=gene, + score=float(score) if score else None, + include_pubmeds=include_pubmeds, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_diseases(self, mirna: Optional[str] = None, page: Optional[int] = None, + page_size: Optional[int] = None) -> Dict: + """Get miRNA disease associations from Modulector SDK.""" + try: + result = modulector.get_diseases( + mirna=mirna, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_drugs(self, mirna: Optional[str] = None, page: Optional[int] = None, + page_size: Optional[int] = None) -> Dict: + """Get drug/miRNA associations from Modulector SDK.""" + try: + result = modulector.get_drugs( + mirna=mirna, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_mirna_codes(self, mirna_codes: List[str]) -> Optional[Dict[str, Optional[str]]]: + """Resolve miRNA identifiers to standard codes via Modulector SDK.""" + try: + return modulector.get_mirna_codes( + mirna_codes=mirna_codes, + base_url=self._modulector_base_url, + ) + except Exception as ex: + logging.exception(ex) + return None + + def find_mirna_codes(self, query: str, limit: Optional[int] = None) -> List[str]: + """Search miRNA identifiers via Modulector SDK.""" + try: + kwargs: Dict[str, Any] = {'query': query, 'base_url': self._modulector_base_url} + if limit is not None: + kwargs['limit'] = limit + return modulector.find_mirna_codes(**kwargs) + except Exception as ex: + logging.exception(ex) + return [] + + def get_methylation_sites(self, methylation_sites: List[str]) -> Optional[Dict[str, List[str]]]: + """Resolve methylation site identifiers to EPIC 2.0 names via Modulector SDK.""" + try: + return modulector.get_methylation_sites( + methylation_sites=methylation_sites, + base_url=self._modulector_base_url, + ) + except Exception as ex: + logging.exception(ex) + return None + + def find_methylation_sites(self, query: str, limit: Optional[int] = None) -> List[str]: + """Search methylation site identifiers via Modulector SDK.""" + try: + kwargs: Dict[str, Any] = {'query': query, 'base_url': self._modulector_base_url} + if limit is not None: + kwargs['limit'] = limit + return modulector.find_methylation_sites(**kwargs) + except Exception as ex: + logging.exception(ex) + return [] -global_mrna_service = MRNAService() +global_mrna_service = MRNAService() \ No newline at end of file diff --git a/src/api_service/views.py b/src/api_service/views.py index 901da96b..0f0a3c6b 100644 --- a/src/api_service/views.py +++ b/src/api_service/views.py @@ -914,21 +914,23 @@ def get_number_samples_in_common_action_one_front(request): @login_required def mirna_data_action(request): """Gets miRNA data from Modulector""" - data = global_mrna_service.get_modulector_service_content('mirna', request.GET, is_paginated=False) + data = global_mrna_service.get_mirna_details(mirna=request.GET.get('mirna', '')) return generate_json_response_or_404(data) @login_required def methylation_data_action(request): """Gets Methylation site data from Modulector""" - data = global_mrna_service.get_modulector_service_content('methylation', request.GET, is_paginated=False) + data = global_mrna_service.get_methylation_details( + methylation_site=request.GET.get('methylation_site', '') + ) return generate_json_response_or_404(data) @login_required def get_mirna_target_interaction_action(request): """ - Searches in papers an specific miRNA interaction. + Searches in papers an specific miRNA interaction. Examples: http://127.0.0.1:8000/api-service/mirna-target-interaction?gene=BRCA1&mirna=hsa-miR-132-3p&include_pubmeds=true @@ -939,48 +941,40 @@ def get_mirna_target_interaction_action(request): """ gene = request.GET.get('gene') mirna = request.GET.get('mirna') - include_pubmeds = request.GET.get('include_pubmeds') if not gene and not mirna: return JsonResponse(data={"error": "Param 'mirna' or 'gene' are mandatory"}, status=400) - if gene and not mirna: - params = {'gene': gene} - elif not gene and mirna: - params = {'mirna': mirna} - else: - params = {'mirna': mirna, 'gene': gene} - - score = request.GET.get('score') - if score: - params['score'] = score - - if include_pubmeds: - if include_pubmeds.lower() == "true": - params['include_pubmeds'] = "true" - - # Gets include_pubmeds flag - include_pubmeds = request.GET.get('include_pubmeds') == 'true' - if include_pubmeds: - params['include_pubmeds'] = 'true' - data = global_mrna_service.get_modulector_service_content('mirna-target-interactions', - request_params=params, - is_paginated=True, - method='get') + data = global_mrna_service.get_mirna_target_interactions( + mirna=mirna, + gene=gene, + score=request.GET.get('score'), + include_pubmeds=request.GET.get('include_pubmeds', '').lower() == 'true', + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return JsonResponse(data) @login_required def get_mirna_diseases_action(request): """Searches in papers miRNA associations with diseases""" - data = global_mrna_service.get_modulector_service_content('diseases', request.GET, is_paginated=True) + data = global_mrna_service.get_diseases( + mirna=request.GET.get('mirna'), + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return generate_json_response_or_404(data) @login_required def get_mirna_drugs_action(request): """Searches in papers miRNA associations with drugs""" - data = global_mrna_service.get_modulector_service_content('drugs', request.GET, is_paginated=True) + data = global_mrna_service.get_drugs( + mirna=request.GET.get('mirna'), + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return generate_json_response_or_404(data) diff --git a/src/assistant/services/tools.py b/src/assistant/services/tools.py index be3331d3..413457e1 100644 --- a/src/assistant/services/tools.py +++ b/src/assistant/services/tools.py @@ -542,15 +542,9 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 """ from api_service.mrna_service import global_mrna_service - params: dict = {'gene': gene_name} - if min_score > 0: - params['score'] = str(min_score) - - data = global_mrna_service.get_mdulector_service_content( - 'mirna-target-interactions', - request_params=params, - is_paginated=True, - method='get' + data = global_mrna_service.get_mirna_target_interactions( + gene=gene_name, + score=str(min_score) if min_score > 0 else None, ) if not data or data.get('count', 0) == 0: @@ -567,6 +561,122 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 'modulators': results }, default=str) + @tool + def search_mirna(query: str) -> str: + """ + Searches for miRNA identifiers in Modulector by name or partial name. + Use this when the user asks to search, find, or look up a miRNA by name + (e.g. "hsa-miR-132", "miR-21"). Returns a list of matching miRNA codes + and their standard accession IDs. + Always use this before get_mirna_details to confirm the exact identifier. + """ + from api_service.mrna_service import global_mrna_service + + results = global_mrna_service.find_mirna_codes(query=query) + + if not results: + return json.dumps({'message': f'No miRNAs found matching "{query}" in Modulector.'}) + + aliases = global_mrna_service.get_mirna_codes(mirna_codes=results) or {} + data = [{'molecule': m, 'standard': aliases.get(m)} for m in results] + return json.dumps({'query': query, 'count': len(data), 'results': data}, default=str) + + @tool + def get_mirna_details(mirna: str) -> str: + """ + Returns detailed information about a specific miRNA from Modulector, + including its accession ID, sequence, aliases, and database references. + Use this with an exact miRNA identifier obtained from search_mirna. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_mirna_details(mirna=mirna) + + if not data: + return json.dumps({'message': f'No information found for miRNA "{mirna}" in Modulector.'}) + + return json.dumps({'mirna': mirna, 'details': data}, default=str) + + @tool + def get_mirna_target_genes(mirna: str, min_score: float = 0.0, limit: int = 20) -> str: + """ + Returns genes known to be targeted/regulated by a specific miRNA, + sourced from Modulector's miRNA-target interaction database. + Use this when the user asks which genes a miRNA regulates, targets, + or silences, or asks about downstream targets of a miRNA. + Results include gene name, interaction score, and supporting evidence. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_mirna_target_interactions( + mirna=mirna, + score=str(min_score) if min_score > 0 else None, + ) + + if not data or data.get('count', 0) == 0: + return json.dumps({ + 'message': f'No target genes found for miRNA "{mirna}" in Modulector.', + 'results': [] + }) + + results = data.get('results', [])[:limit] + return json.dumps({ + 'mirna': mirna, + 'total_interactions': data.get('count', 0), + 'shown': len(results), + 'targets': results + }, default=str) + + @tool + def get_mirna_diseases(mirna: str, limit: int = 20) -> str: + """ + Returns diseases associated with a specific miRNA from Modulector. + Use this when the user asks about the clinical relevance, pathological + associations, or disease context of a miRNA. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_diseases(mirna=mirna) + + if not data or data.get('count', 0) == 0: + return json.dumps({ + 'message': f'No disease associations found for miRNA "{mirna}" in Modulector.', + 'results': [] + }) + + results = data.get('results', [])[:limit] + return json.dumps({ + 'mirna': mirna, + 'total': data.get('count', 0), + 'shown': len(results), + 'diseases': results + }, default=str) + + @tool + def get_mirna_drugs(mirna: str, limit: int = 20) -> str: + """ + Returns drugs or molecules associated with a specific miRNA from Modulector. + Use this when the user asks about pharmacological context, drug interactions, + or therapeutic relevance of a miRNA. + """ + from api_service.mrna_service import global_mrna_service + + data = global_mrna_service.get_drugs(mirna=mirna) + + if not data or data.get('count', 0) == 0: + return json.dumps({ + 'message': f'No drug associations found for miRNA "{mirna}" in Modulector.', + 'results': [] + }) + + results = data.get('results', [])[:limit] + return json.dumps({ + 'mirna': mirna, + 'total': data.get('count', 0), + 'shown': len(results), + 'drugs': results + }, default=str) + @tool def get_gene_annotations(gene_name: str) -> str: """ @@ -632,6 +742,11 @@ def get_drugs_regulating_gene(gene_name: str) -> str: get_gene_info, get_gene_annotations, get_mirna_modulators, + search_mirna, + get_mirna_details, + get_mirna_target_genes, + get_mirna_diseases, + get_mirna_drugs, get_drugs_regulating_gene, get_string_interaction_partners, get_string_functional_enrichment, diff --git a/src/biomarkers/views.py b/src/biomarkers/views.py index c67d9324..67c0ea54 100644 --- a/src/biomarkers/views.py +++ b/src/biomarkers/views.py @@ -168,6 +168,8 @@ def find_genes_from_request(request: Request) -> List[Dict]: """ genes_found = global_mrna_service.get_bioapi_service_content('gene-symbols-finder', request.GET, is_paginated=False) + if not genes_found: + return [] aliases = get_gene_aliases(genes_found) return [{'molecule': gene, 'standard': aliases.get(gene, [None])[0]} for gene in genes_found] @@ -197,17 +199,11 @@ class MiRNACodes(APIView): @staticmethod def __get_mirna_aliases(mirna_codes: List[str]) -> Optional[Dict]: """Get the aliases for a list of miRNAs through Modulector""" - return global_mrna_service.get_modulector_service_content( - 'mirna-codes', - request_params={'mirna_codes': mirna_codes}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_mirna_codes(mirna_codes=mirna_codes) def get(self, request): """Generates a query to search miRNAs through Modulector""" - mirnas_found = global_mrna_service.get_modulector_service_content('mirna-codes-finder', - request.GET, is_paginated=False) + mirnas_found = global_mrna_service.find_mirna_codes(query=request.GET.get('query', '')) # Generates the structure for the frontend aliases = self.__get_mirna_aliases(mirnas_found) @@ -235,23 +231,12 @@ class MethylationSites(APIView): @staticmethod def __get_methylation_sites_aliases(methylation_sites: List[str]) -> Optional[Dict]: """Get the aliases for a list of Methylation sites through Modulector""" - return global_mrna_service.get_modulector_service_content( - 'methylation-sites', - request_params={'methylation_sites': methylation_sites}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_methylation_sites(methylation_sites=methylation_sites) def get(self, request: Request): """Generates a query to search Methylation sites through Modulector""" - # methylation_sites = request.GET.get('methylation_sites', '') - # Gets the Methylation sites - sites_found = global_mrna_service.get_modulector_service_content( - 'methylation-sites-finder', - request_params=request.GET, - is_paginated=False - ) + sites_found = global_mrna_service.find_methylation_sites(query=request.GET.get('query', '')) # Generates the structure for the frontend aliases = self.__get_methylation_sites_aliases(sites_found) diff --git a/src/molecules_details/views.py b/src/molecules_details/views.py index 1d4c9c13..7fc61404 100644 --- a/src/molecules_details/views.py +++ b/src/molecules_details/views.py @@ -594,14 +594,7 @@ def get(request: HttpRequest): if not methylation_site: return Response(status=400, data={"error": "Param 'methylation_site' is mandatory"}) - data = global_mrna_service.get_modulector_service_content( - 'methylation', - request_params={ - 'methylation_site': methylation_site - }, - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_methylation_details(methylation_site=methylation_site) return Response({ 'data': data if data else None diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index 52f39191..5f4737df 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -304,11 +304,15 @@ EMAIL_PAGE_DOMAIN = 'https://multiomix.org' # Modulector settings -MODULECTOR_SETTINGS = { - 'host': os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org'), - 'port': os.getenv('MODULECTOR_PORT', 443), - 'protocol': os.getenv('BIOAPI_PROTOCOL', 'https') -} +_modulector_protocol = os.getenv('MODULECTOR_PROTOCOL', os.getenv('BIOAPI_PROTOCOL', 'https')) +_modulector_host = os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org') +_modulector_port = os.getenv('MODULECTOR_PORT', '443') +# MODULECTOR_API_BASE_URL is also read directly by the modulector-sdk as env var +MODULECTOR_BASE_URL = os.getenv( + 'MODULECTOR_API_BASE_URL', + f"{_modulector_protocol}://{_modulector_host}" if _modulector_port in ('80', '443') + else f"{_modulector_protocol}://{_modulector_host}:{_modulector_port}" +) # BioAPI settings BIOAPI_SETTINGS = { From 83094adaa8852a9f50eb679460db73a2fb0a7ee5 Mon Sep 17 00:00:00 2001 From: Hernan Date: Thu, 30 Jul 2026 20:46:06 -0300 Subject: [PATCH 2/2] Move miRNA tools to Modulector MCP server Enable a dedicated `modulector` MCP server in `mcp_servers.json` and remove the in-process miRNA helper tools from `tools.py` (search/details/targets/diseases/drugs), along with their registration. This centralizes miRNA and related biological lookups through the external Modulector MCP integration instead of local assistant wrappers. --- config/mcp_servers.json | 10 +++ src/assistant/services/tools.py | 121 -------------------------------- 2 files changed, 10 insertions(+), 121 deletions(-) diff --git a/config/mcp_servers.json b/config/mcp_servers.json index 53313b39..b16263e7 100644 --- a/config/mcp_servers.json +++ b/config/mcp_servers.json @@ -8,6 +8,16 @@ "command": "biomcp-cli", "args": ["mcp"], "env": {} + }, + "modulector": { + "enabled": true, + "description": "miRNA target interactions, aliases, methylation sites, disease and drug associations", + "transport": "stdio", + "command": "modulector-mcp", + "args": [], + "env": { + "MODULECTOR_API_BASE_URL": "https://modulector.multiomix.org" + } } } } diff --git a/src/assistant/services/tools.py b/src/assistant/services/tools.py index 413457e1..39b7997e 100644 --- a/src/assistant/services/tools.py +++ b/src/assistant/services/tools.py @@ -561,122 +561,6 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 'modulators': results }, default=str) - @tool - def search_mirna(query: str) -> str: - """ - Searches for miRNA identifiers in Modulector by name or partial name. - Use this when the user asks to search, find, or look up a miRNA by name - (e.g. "hsa-miR-132", "miR-21"). Returns a list of matching miRNA codes - and their standard accession IDs. - Always use this before get_mirna_details to confirm the exact identifier. - """ - from api_service.mrna_service import global_mrna_service - - results = global_mrna_service.find_mirna_codes(query=query) - - if not results: - return json.dumps({'message': f'No miRNAs found matching "{query}" in Modulector.'}) - - aliases = global_mrna_service.get_mirna_codes(mirna_codes=results) or {} - data = [{'molecule': m, 'standard': aliases.get(m)} for m in results] - return json.dumps({'query': query, 'count': len(data), 'results': data}, default=str) - - @tool - def get_mirna_details(mirna: str) -> str: - """ - Returns detailed information about a specific miRNA from Modulector, - including its accession ID, sequence, aliases, and database references. - Use this with an exact miRNA identifier obtained from search_mirna. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_mirna_details(mirna=mirna) - - if not data: - return json.dumps({'message': f'No information found for miRNA "{mirna}" in Modulector.'}) - - return json.dumps({'mirna': mirna, 'details': data}, default=str) - - @tool - def get_mirna_target_genes(mirna: str, min_score: float = 0.0, limit: int = 20) -> str: - """ - Returns genes known to be targeted/regulated by a specific miRNA, - sourced from Modulector's miRNA-target interaction database. - Use this when the user asks which genes a miRNA regulates, targets, - or silences, or asks about downstream targets of a miRNA. - Results include gene name, interaction score, and supporting evidence. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_mirna_target_interactions( - mirna=mirna, - score=str(min_score) if min_score > 0 else None, - ) - - if not data or data.get('count', 0) == 0: - return json.dumps({ - 'message': f'No target genes found for miRNA "{mirna}" in Modulector.', - 'results': [] - }) - - results = data.get('results', [])[:limit] - return json.dumps({ - 'mirna': mirna, - 'total_interactions': data.get('count', 0), - 'shown': len(results), - 'targets': results - }, default=str) - - @tool - def get_mirna_diseases(mirna: str, limit: int = 20) -> str: - """ - Returns diseases associated with a specific miRNA from Modulector. - Use this when the user asks about the clinical relevance, pathological - associations, or disease context of a miRNA. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_diseases(mirna=mirna) - - if not data or data.get('count', 0) == 0: - return json.dumps({ - 'message': f'No disease associations found for miRNA "{mirna}" in Modulector.', - 'results': [] - }) - - results = data.get('results', [])[:limit] - return json.dumps({ - 'mirna': mirna, - 'total': data.get('count', 0), - 'shown': len(results), - 'diseases': results - }, default=str) - - @tool - def get_mirna_drugs(mirna: str, limit: int = 20) -> str: - """ - Returns drugs or molecules associated with a specific miRNA from Modulector. - Use this when the user asks about pharmacological context, drug interactions, - or therapeutic relevance of a miRNA. - """ - from api_service.mrna_service import global_mrna_service - - data = global_mrna_service.get_drugs(mirna=mirna) - - if not data or data.get('count', 0) == 0: - return json.dumps({ - 'message': f'No drug associations found for miRNA "{mirna}" in Modulector.', - 'results': [] - }) - - results = data.get('results', [])[:limit] - return json.dumps({ - 'mirna': mirna, - 'total': data.get('count', 0), - 'shown': len(results), - 'drugs': results - }, default=str) - @tool def get_gene_annotations(gene_name: str) -> str: """ @@ -742,11 +626,6 @@ def get_drugs_regulating_gene(gene_name: str) -> str: get_gene_info, get_gene_annotations, get_mirna_modulators, - search_mirna, - get_mirna_details, - get_mirna_target_genes, - get_mirna_diseases, - get_mirna_drugs, get_drugs_regulating_gene, get_string_interaction_partners, get_string_functional_enrichment,