tooluniverse 1.0.9.1__py3-none-any.whl → 1.0.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tooluniverse might be problematic. Click here for more details.

Files changed (191) hide show
  1. tooluniverse/__init__.py +57 -1
  2. tooluniverse/admetai_tool.py +1 -1
  3. tooluniverse/agentic_tool.py +65 -17
  4. tooluniverse/base_tool.py +19 -8
  5. tooluniverse/blast_tool.py +132 -0
  6. tooluniverse/boltz_tool.py +3 -3
  7. tooluniverse/cache/result_cache_manager.py +167 -12
  8. tooluniverse/cbioportal_tool.py +42 -0
  9. tooluniverse/clinvar_tool.py +268 -74
  10. tooluniverse/compose_scripts/drug_safety_analyzer.py +1 -1
  11. tooluniverse/compose_scripts/multi_agent_literature_search.py +1 -1
  12. tooluniverse/compose_scripts/output_summarizer.py +4 -4
  13. tooluniverse/compose_scripts/tool_discover.py +1941 -443
  14. tooluniverse/compose_scripts/tool_graph_composer.py +1 -1
  15. tooluniverse/compose_scripts/tool_metadata_generator.py +1 -1
  16. tooluniverse/compose_tool.py +9 -9
  17. tooluniverse/core_tool.py +2 -2
  18. tooluniverse/ctg_tool.py +4 -4
  19. tooluniverse/custom_tool.py +1 -1
  20. tooluniverse/data/agentic_tools.json +0 -370
  21. tooluniverse/data/alphafold_tools.json +6 -6
  22. tooluniverse/data/blast_tools.json +112 -0
  23. tooluniverse/data/cbioportal_tools.json +87 -0
  24. tooluniverse/data/clinvar_tools.json +235 -0
  25. tooluniverse/data/compose_tools.json +0 -89
  26. tooluniverse/data/dbsnp_tools.json +275 -0
  27. tooluniverse/data/emdb_tools.json +61 -0
  28. tooluniverse/data/ensembl_tools.json +259 -0
  29. tooluniverse/data/file_download_tools.json +275 -0
  30. tooluniverse/data/geo_tools.json +200 -48
  31. tooluniverse/data/gnomad_tools.json +109 -0
  32. tooluniverse/data/gtopdb_tools.json +68 -0
  33. tooluniverse/data/gwas_tools.json +32 -0
  34. tooluniverse/data/interpro_tools.json +199 -0
  35. tooluniverse/data/jaspar_tools.json +70 -0
  36. tooluniverse/data/kegg_tools.json +356 -0
  37. tooluniverse/data/mpd_tools.json +87 -0
  38. tooluniverse/data/ols_tools.json +314 -0
  39. tooluniverse/data/package_discovery_tools.json +64 -0
  40. tooluniverse/data/packages/categorized_tools.txt +0 -1
  41. tooluniverse/data/packages/machine_learning_tools.json +0 -47
  42. tooluniverse/data/paleobiology_tools.json +91 -0
  43. tooluniverse/data/pride_tools.json +62 -0
  44. tooluniverse/data/pypi_package_inspector_tools.json +158 -0
  45. tooluniverse/data/python_executor_tools.json +341 -0
  46. tooluniverse/data/regulomedb_tools.json +50 -0
  47. tooluniverse/data/remap_tools.json +89 -0
  48. tooluniverse/data/screen_tools.json +89 -0
  49. tooluniverse/data/tool_discovery_agents.json +428 -0
  50. tooluniverse/data/tool_discovery_agents.json.backup +1343 -0
  51. tooluniverse/data/uniprot_tools.json +77 -0
  52. tooluniverse/data/web_search_tools.json +250 -0
  53. tooluniverse/data/worms_tools.json +55 -0
  54. tooluniverse/dataset_tool.py +2 -2
  55. tooluniverse/dbsnp_tool.py +196 -58
  56. tooluniverse/default_config.py +36 -3
  57. tooluniverse/emdb_tool.py +30 -0
  58. tooluniverse/enrichr_tool.py +14 -14
  59. tooluniverse/ensembl_tool.py +140 -47
  60. tooluniverse/execute_function.py +594 -29
  61. tooluniverse/extended_hooks.py +4 -4
  62. tooluniverse/file_download_tool.py +269 -0
  63. tooluniverse/gene_ontology_tool.py +1 -1
  64. tooluniverse/generate_tools.py +3 -3
  65. tooluniverse/geo_tool.py +81 -28
  66. tooluniverse/gnomad_tool.py +100 -52
  67. tooluniverse/gtopdb_tool.py +41 -0
  68. tooluniverse/humanbase_tool.py +10 -10
  69. tooluniverse/interpro_tool.py +72 -0
  70. tooluniverse/jaspar_tool.py +30 -0
  71. tooluniverse/kegg_tool.py +230 -0
  72. tooluniverse/logging_config.py +2 -2
  73. tooluniverse/mcp_client_tool.py +57 -129
  74. tooluniverse/mcp_integration.py +52 -49
  75. tooluniverse/mcp_tool_registry.py +147 -528
  76. tooluniverse/mpd_tool.py +42 -0
  77. tooluniverse/ncbi_eutils_tool.py +96 -0
  78. tooluniverse/ols_tool.py +435 -0
  79. tooluniverse/openalex_tool.py +8 -8
  80. tooluniverse/openfda_tool.py +2 -2
  81. tooluniverse/output_hook.py +15 -15
  82. tooluniverse/package_discovery_tool.py +217 -0
  83. tooluniverse/package_tool.py +1 -1
  84. tooluniverse/paleobiology_tool.py +30 -0
  85. tooluniverse/pmc_tool.py +2 -2
  86. tooluniverse/pride_tool.py +30 -0
  87. tooluniverse/pypi_package_inspector_tool.py +593 -0
  88. tooluniverse/python_executor_tool.py +711 -0
  89. tooluniverse/regulomedb_tool.py +30 -0
  90. tooluniverse/remap_tool.py +44 -0
  91. tooluniverse/remote/boltz/boltz_mcp_server.py +1 -1
  92. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +3 -3
  93. tooluniverse/remote/immune_compass/compass_tool.py +3 -3
  94. tooluniverse/remote/pinnacle/pinnacle_tool.py +2 -2
  95. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +3 -3
  96. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +3 -3
  97. tooluniverse/remote_tool.py +4 -4
  98. tooluniverse/screen_tool.py +44 -0
  99. tooluniverse/scripts/filter_tool_files.py +2 -2
  100. tooluniverse/smcp.py +93 -12
  101. tooluniverse/smcp_server.py +100 -21
  102. tooluniverse/space/__init__.py +46 -0
  103. tooluniverse/space/loader.py +133 -0
  104. tooluniverse/space/validator.py +353 -0
  105. tooluniverse/tool_finder_embedding.py +5 -3
  106. tooluniverse/tool_finder_keyword.py +12 -10
  107. tooluniverse/tool_finder_llm.py +12 -8
  108. tooluniverse/tools/{UCSC_get_genes_by_region.py → BLAST_nucleotide_search.py} +22 -26
  109. tooluniverse/tools/BLAST_protein_search.py +63 -0
  110. tooluniverse/tools/ClinVar_search_variants.py +26 -15
  111. tooluniverse/tools/CodeQualityAnalyzer.py +3 -3
  112. tooluniverse/tools/EMDB_get_structure.py +46 -0
  113. tooluniverse/tools/GtoPdb_get_targets.py +52 -0
  114. tooluniverse/tools/InterPro_get_domain_details.py +46 -0
  115. tooluniverse/tools/InterPro_get_protein_domains.py +49 -0
  116. tooluniverse/tools/InterPro_search_domains.py +52 -0
  117. tooluniverse/tools/JASPAR_get_transcription_factors.py +52 -0
  118. tooluniverse/tools/MPD_get_phenotype_data.py +59 -0
  119. tooluniverse/tools/PRIDE_search_proteomics.py +52 -0
  120. tooluniverse/tools/PackageAnalyzer.py +55 -0
  121. tooluniverse/tools/Paleobiology_get_fossils.py +52 -0
  122. tooluniverse/tools/PyPIPackageInspector.py +59 -0
  123. tooluniverse/tools/ReMap_get_transcription_factor_binding.py +59 -0
  124. tooluniverse/tools/ReferenceInfoAnalyzer.py +55 -0
  125. tooluniverse/tools/RegulomeDB_query_variant.py +46 -0
  126. tooluniverse/tools/SCREEN_get_regulatory_elements.py +59 -0
  127. tooluniverse/tools/{ArgumentDescriptionOptimizer.py → TestResultsAnalyzer.py} +13 -13
  128. tooluniverse/tools/ToolDiscover.py +11 -11
  129. tooluniverse/tools/UniProt_id_mapping.py +63 -0
  130. tooluniverse/tools/UniProt_search.py +63 -0
  131. tooluniverse/tools/UnifiedToolGenerator.py +59 -0
  132. tooluniverse/tools/WoRMS_search_species.py +49 -0
  133. tooluniverse/tools/XMLToolOptimizer.py +55 -0
  134. tooluniverse/tools/__init__.py +119 -29
  135. tooluniverse/tools/_shared_client.py +3 -3
  136. tooluniverse/tools/alphafold_get_annotations.py +3 -3
  137. tooluniverse/tools/alphafold_get_prediction.py +3 -3
  138. tooluniverse/tools/alphafold_get_summary.py +3 -3
  139. tooluniverse/tools/cBioPortal_get_cancer_studies.py +46 -0
  140. tooluniverse/tools/cBioPortal_get_mutations.py +52 -0
  141. tooluniverse/tools/{gnomAD_query_variant.py → clinvar_get_clinical_significance.py} +8 -11
  142. tooluniverse/tools/clinvar_get_variant_details.py +49 -0
  143. tooluniverse/tools/dbSNP_get_variant_by_rsid.py +7 -7
  144. tooluniverse/tools/dbsnp_get_frequencies.py +46 -0
  145. tooluniverse/tools/dbsnp_search_by_gene.py +52 -0
  146. tooluniverse/tools/download_binary_file.py +66 -0
  147. tooluniverse/tools/download_file.py +71 -0
  148. tooluniverse/tools/download_text_content.py +55 -0
  149. tooluniverse/tools/dynamic_package_discovery.py +59 -0
  150. tooluniverse/tools/ensembl_get_sequence.py +52 -0
  151. tooluniverse/tools/{Ensembl_lookup_gene_by_symbol.py → ensembl_get_variants.py} +11 -11
  152. tooluniverse/tools/ensembl_lookup_gene.py +46 -0
  153. tooluniverse/tools/geo_get_dataset_info.py +46 -0
  154. tooluniverse/tools/geo_get_sample_info.py +46 -0
  155. tooluniverse/tools/geo_search_datasets.py +67 -0
  156. tooluniverse/tools/gnomad_get_gene_constraints.py +49 -0
  157. tooluniverse/tools/kegg_find_genes.py +52 -0
  158. tooluniverse/tools/kegg_get_gene_info.py +46 -0
  159. tooluniverse/tools/kegg_get_pathway_info.py +46 -0
  160. tooluniverse/tools/kegg_list_organisms.py +44 -0
  161. tooluniverse/tools/kegg_search_pathway.py +46 -0
  162. tooluniverse/tools/ols_find_similar_terms.py +63 -0
  163. tooluniverse/tools/{get_hyperopt_info.py → ols_get_ontology_info.py} +13 -10
  164. tooluniverse/tools/ols_get_term_ancestors.py +67 -0
  165. tooluniverse/tools/ols_get_term_children.py +67 -0
  166. tooluniverse/tools/{TestCaseGenerator.py → ols_get_term_info.py} +12 -9
  167. tooluniverse/tools/{CodeOptimizer.py → ols_search_ontologies.py} +22 -14
  168. tooluniverse/tools/ols_search_terms.py +71 -0
  169. tooluniverse/tools/python_code_executor.py +79 -0
  170. tooluniverse/tools/python_script_runner.py +79 -0
  171. tooluniverse/tools/web_api_documentation_search.py +63 -0
  172. tooluniverse/tools/web_search.py +71 -0
  173. tooluniverse/uniprot_tool.py +219 -16
  174. tooluniverse/url_tool.py +19 -1
  175. tooluniverse/uspto_tool.py +1 -1
  176. tooluniverse/utils.py +12 -12
  177. tooluniverse/web_search_tool.py +229 -0
  178. tooluniverse/worms_tool.py +64 -0
  179. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/METADATA +8 -3
  180. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/RECORD +184 -92
  181. tooluniverse/data/genomics_tools.json +0 -174
  182. tooluniverse/tools/ToolDescriptionOptimizer.py +0 -67
  183. tooluniverse/tools/ToolImplementationGenerator.py +0 -67
  184. tooluniverse/tools/ToolOptimizer.py +0 -59
  185. tooluniverse/tools/ToolSpecificationGenerator.py +0 -67
  186. tooluniverse/tools/ToolSpecificationOptimizer.py +0 -63
  187. tooluniverse/ucsc_tool.py +0 -60
  188. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/WHEEL +0 -0
  189. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/entry_points.txt +0 -0
  190. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/licenses/LICENSE +0 -0
  191. {tooluniverse-1.0.9.1.dist-info → tooluniverse-1.0.11.dist-info}/top_level.txt +0 -0
@@ -35,19 +35,19 @@ class HumanBaseTool(BaseTool):
35
35
  """
36
36
  Retrieve the official gene symbol (same as EnrichrTool method)
37
37
 
38
- Parameters:
38
+ Parameters
39
39
  gene_name (str): The gene name or synonym to query.
40
40
 
41
- Returns:
41
+ Returns
42
42
  str: The official gene symbol.
43
43
  """
44
44
  """
45
45
  Retrieve the official gene symbol for a given gene name or synonym using the MyGene.info API.
46
46
 
47
- Parameters:
47
+ Parameters
48
48
  gene_name (str): The gene name or synonym to query.
49
49
 
50
- Returns:
50
+ Returns
51
51
  str: The official gene symbol if found; otherwise, raises an Exception.
52
52
  """
53
53
  # URL-encode the gene_name to handle special characters
@@ -96,10 +96,10 @@ class HumanBaseTool(BaseTool):
96
96
  """
97
97
  Convert gene names to Entrez IDs using NCBI Entrez API.
98
98
 
99
- Parameters:
99
+ Parameters
100
100
  gene_names (list): List of gene names to convert.
101
101
 
102
- Returns:
102
+ Returns
103
103
  list: List of Entrez IDs corresponding to the gene names.
104
104
  """
105
105
  # Define the NCBI Entrez API URL for querying gene information
@@ -149,13 +149,13 @@ class HumanBaseTool(BaseTool):
149
149
  """
150
150
  Retrieve protein-protein interactions and biological processes from HumanBase.
151
151
 
152
- Parameters:
152
+ Parameters
153
153
  genes (list): List of gene names to analyze.
154
154
  tissue (str): Tissue type for tissue-specific interactions.
155
155
  max_node (int): Maximum number of nodes to retrieve.
156
156
  interaction (str): Specific interaction type to filter by.
157
157
 
158
- Returns:
158
+ Returns
159
159
  tuple: (NetworkX Graph of interactions, list of biological processes)
160
160
  """
161
161
  genes = self.get_entrez_ids(genes)
@@ -243,13 +243,13 @@ class HumanBaseTool(BaseTool):
243
243
  """
244
244
  Convert NetworkX graph and biological processes to string representation.
245
245
 
246
- Parameters:
246
+ Parameters
247
247
  graph (networkx.Graph): The network graph.
248
248
  bp_collection (list): List of biological processes.
249
249
  original_genes (list): Original gene list provided by user.
250
250
  tissue (str): Tissue type used for analysis.
251
251
 
252
- Returns:
252
+ Returns
253
253
  str: Comprehensive string representation of the network data.
254
254
  """
255
255
  output = []
@@ -0,0 +1,72 @@
1
+ import requests
2
+ from typing import Any, Dict
3
+ from .base_tool import BaseTool
4
+ from .tool_registry import register_tool
5
+
6
+
7
+ @register_tool("InterProRESTTool")
8
+ class InterProRESTTool(BaseTool):
9
+ def __init__(self, tool_config: Dict):
10
+ super().__init__(tool_config)
11
+ self.base_url = "https://www.ebi.ac.uk/interpro/api"
12
+ self.session = requests.Session()
13
+ self.session.headers.update(
14
+ {"Accept": "application/json", "User-Agent": "ToolUniverse/1.0"}
15
+ )
16
+ self.timeout = 30
17
+
18
+ def _build_url(self, args: Dict[str, Any]) -> str:
19
+ """Build URL from endpoint template and arguments"""
20
+ url = self.tool_config["fields"]["endpoint"]
21
+ for k, v in args.items():
22
+ url = url.replace(f"{{{k}}}", str(v))
23
+ return url
24
+
25
+ def _extract_data(self, data: Dict, extract_path: str = None) -> Any:
26
+ """Extract specific data from API response"""
27
+ if not extract_path:
28
+ return data
29
+
30
+ # Handle specific InterPro extraction patterns
31
+ if extract_path == "results":
32
+ return data.get("results", [])
33
+ elif extract_path == "count":
34
+ return data.get("count", 0)
35
+ elif extract_path == "metadata":
36
+ return data.get("metadata", {})
37
+
38
+ return data
39
+
40
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
41
+ """Execute the InterPro API call"""
42
+ try:
43
+ # Build URL from endpoint template
44
+ url = self._build_url(arguments)
45
+
46
+ # Make API request
47
+ response = self.session.get(url, timeout=self.timeout)
48
+ response.raise_for_status()
49
+
50
+ # Parse JSON response
51
+ data = response.json()
52
+
53
+ # Extract data if specified
54
+ extract_path = self.tool_config["fields"].get("extract_path")
55
+ if extract_path:
56
+ result = self._extract_data(data, extract_path)
57
+ else:
58
+ result = data
59
+
60
+ return {
61
+ "status": "success",
62
+ "data": result,
63
+ "url": url,
64
+ "count": len(result) if isinstance(result, list) else 1,
65
+ }
66
+
67
+ except Exception as e:
68
+ return {
69
+ "status": "error",
70
+ "error": f"InterPro API error: {str(e)}",
71
+ "url": url,
72
+ }
@@ -0,0 +1,30 @@
1
+ import requests
2
+ from typing import Any, Dict
3
+ from .base_tool import BaseTool
4
+ from .tool_registry import register_tool
5
+
6
+
7
+ @register_tool("JASPARRESTTool")
8
+ class JASPARRESTTool(BaseTool):
9
+ def __init__(self, tool_config: Dict):
10
+ super().__init__(tool_config)
11
+ self.base_url = "https://jaspar.elixir.no/api/v1"
12
+ self.session = requests.Session()
13
+ self.session.headers.update({"Accept": "application/json"})
14
+ self.timeout = 30
15
+
16
+ def _build_url(self, args: Dict[str, Any]) -> str:
17
+ url = self.tool_config["fields"]["endpoint"]
18
+ for k, v in args.items():
19
+ url = url.replace(f"{{{k}}}", str(v))
20
+ return url
21
+
22
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
23
+ try:
24
+ url = self._build_url(arguments)
25
+ response = self.session.get(url, timeout=self.timeout)
26
+ response.raise_for_status()
27
+ data = response.json()
28
+ return {"status": "success", "data": data, "url": url}
29
+ except Exception as e:
30
+ return {"status": "error", "error": f"JASPAR API error: {str(e)}"}
@@ -0,0 +1,230 @@
1
+ """
2
+ KEGG Database REST API Tool
3
+
4
+ This tool provides access to the KEGG (Kyoto Encyclopedia of Genes and Genomes)
5
+ database for pathway analysis, gene information, and organism data.
6
+ """
7
+
8
+ import requests
9
+ from typing import Dict, Any, Optional
10
+ from .base_tool import BaseTool
11
+ from .tool_registry import register_tool
12
+
13
+
14
+ class KEGGRESTTool(BaseTool):
15
+ """Base class for KEGG REST API tools."""
16
+
17
+ def __init__(self, tool_config):
18
+ super().__init__(tool_config)
19
+ self.base_url = "https://rest.kegg.jp"
20
+ self.session = requests.Session()
21
+ self.session.headers.update(
22
+ {"Accept": "text/plain, application/json", "User-Agent": "ToolUniverse/1.0"}
23
+ )
24
+ self.timeout = 30
25
+
26
+ def _make_request(
27
+ self, endpoint: str, params: Optional[Dict] = None
28
+ ) -> Dict[str, Any]:
29
+ """Make a request to the KEGG API."""
30
+ url = f"{self.base_url}{endpoint}"
31
+ try:
32
+ response = self.session.get(url, params=params, timeout=self.timeout)
33
+ response.raise_for_status()
34
+
35
+ # KEGG API returns text/plain by default, parse as text
36
+ content = response.text.strip()
37
+
38
+ # Try to parse as structured data if possible
39
+ if content.startswith("{") or content.startswith("["):
40
+ try:
41
+ return {"status": "success", "data": response.json()}
42
+ except Exception:
43
+ pass
44
+
45
+ # Return as text data
46
+ return {
47
+ "status": "success",
48
+ "data": content,
49
+ "url": url,
50
+ "content_type": response.headers.get("content-type", "text/plain"),
51
+ }
52
+
53
+ except requests.exceptions.RequestException as e:
54
+ return {
55
+ "status": "error",
56
+ "error": f"KEGG API request failed: {str(e)}",
57
+ "url": url,
58
+ }
59
+
60
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
61
+ """Execute the tool with given arguments."""
62
+ return self._make_request(self.endpoint, arguments)
63
+
64
+
65
+ @register_tool("KEGGSearchPathway")
66
+ class KEGGSearchPathway(KEGGRESTTool):
67
+ """Search KEGG pathways by keyword."""
68
+
69
+ def __init__(self, tool_config):
70
+ super().__init__(tool_config)
71
+ self.endpoint = "/find/pathway"
72
+
73
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
74
+ """Search pathways with keyword."""
75
+ keyword = arguments.get("keyword", "")
76
+ if not keyword:
77
+ return {"status": "error", "error": "keyword is required"}
78
+
79
+ # KEGG API requires the search term in the URL path
80
+ endpoint = f"{self.endpoint}/{keyword}"
81
+ result = self._make_request(endpoint)
82
+
83
+ # Parse pathway results
84
+ if result.get("status") == "success" and isinstance(result.get("data"), str):
85
+ lines = result["data"].split("\n")
86
+ pathways = []
87
+ for line in lines:
88
+ if "\t" in line:
89
+ parts = line.split("\t", 1)
90
+ if len(parts) == 2:
91
+ pathways.append(
92
+ {"pathway_id": parts[0], "description": parts[1]}
93
+ )
94
+ result["data"] = pathways
95
+ result["count"] = len(pathways)
96
+
97
+ return result
98
+
99
+
100
+ @register_tool("KEGGGetPathwayInfo")
101
+ class KEGGGetPathwayInfo(KEGGRESTTool):
102
+ """Get detailed pathway information by pathway ID."""
103
+
104
+ def __init__(self, tool_config):
105
+ super().__init__(tool_config)
106
+ self.endpoint = "/get"
107
+
108
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
109
+ """Get pathway information."""
110
+ pathway_id = arguments.get("pathway_id", "")
111
+ if not pathway_id:
112
+ return {"status": "error", "error": "pathway_id is required"}
113
+
114
+ # Add pathway prefix if not present
115
+ if not pathway_id.startswith("path:"):
116
+ pathway_id = f"path:{pathway_id}"
117
+
118
+ # KEGG API requires the ID in the URL path
119
+ endpoint = f"{self.endpoint}/{pathway_id}"
120
+ result = self._make_request(endpoint)
121
+
122
+ # Parse pathway data
123
+ if result.get("status") == "success" and isinstance(result.get("data"), str):
124
+ lines = result["data"].split("\n")
125
+ pathway_info = {
126
+ "pathway_id": pathway_id,
127
+ "raw_data": result["data"],
128
+ "lines": len(lines),
129
+ }
130
+ result["data"] = pathway_info
131
+
132
+ return result
133
+
134
+
135
+ @register_tool("KEGGFindGenes")
136
+ class KEGGFindGenes(KEGGRESTTool):
137
+ """Find genes by keyword in KEGG database."""
138
+
139
+ def __init__(self, tool_config):
140
+ super().__init__(tool_config)
141
+ self.endpoint = "/find/genes"
142
+
143
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
144
+ """Find genes with keyword."""
145
+ keyword = arguments.get("keyword", "")
146
+ if not keyword:
147
+ return {"status": "error", "error": "keyword is required"}
148
+
149
+ # KEGG API requires the search term in the URL path
150
+ # For gene search, we don't need organism prefix in the URL
151
+ endpoint = f"{self.endpoint}/{keyword}"
152
+ result = self._make_request(endpoint)
153
+
154
+ # Parse gene results
155
+ if result.get("status") == "success" and isinstance(result.get("data"), str):
156
+ lines = result["data"].split("\n")
157
+ genes = []
158
+ for line in lines:
159
+ if "\t" in line:
160
+ parts = line.split("\t", 1)
161
+ if len(parts) == 2:
162
+ genes.append({"gene_id": parts[0], "description": parts[1]})
163
+ result["data"] = genes
164
+ result["count"] = len(genes)
165
+
166
+ return result
167
+
168
+
169
+ @register_tool("KEGGGetGeneInfo")
170
+ class KEGGGetGeneInfo(KEGGRESTTool):
171
+ """Get detailed gene information by gene ID."""
172
+
173
+ def __init__(self, tool_config):
174
+ super().__init__(tool_config)
175
+ self.endpoint = "/get"
176
+
177
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
178
+ """Get gene information."""
179
+ gene_id = arguments.get("gene_id", "")
180
+ if not gene_id:
181
+ return {"status": "error", "error": "gene_id is required"}
182
+
183
+ # KEGG API requires the ID in the URL path
184
+ endpoint = f"{self.endpoint}/{gene_id}"
185
+ result = self._make_request(endpoint)
186
+
187
+ # Parse gene data
188
+ if result.get("status") == "success" and isinstance(result.get("data"), str):
189
+ lines = result["data"].split("\n")
190
+ gene_info = {
191
+ "gene_id": gene_id,
192
+ "raw_data": result["data"],
193
+ "lines": len(lines),
194
+ }
195
+ result["data"] = gene_info
196
+
197
+ return result
198
+
199
+
200
+ @register_tool("KEGGListOrganisms")
201
+ class KEGGListOrganisms(KEGGRESTTool):
202
+ """List available organisms in KEGG database."""
203
+
204
+ def __init__(self, tool_config):
205
+ super().__init__(tool_config)
206
+ self.endpoint = "/list/organism"
207
+
208
+ def run(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
209
+ """List organisms."""
210
+ result = self._make_request(self.endpoint)
211
+
212
+ # Parse organism list
213
+ if result.get("status") == "success" and isinstance(result.get("data"), str):
214
+ lines = result["data"].split("\n")
215
+ organisms = []
216
+ for line in lines:
217
+ if "\t" in line:
218
+ parts = line.split("\t")
219
+ if len(parts) >= 3:
220
+ organisms.append(
221
+ {
222
+ "organism_code": parts[0],
223
+ "organism_name": parts[1],
224
+ "description": parts[2] if len(parts) > 2 else "",
225
+ }
226
+ )
227
+ result["data"] = organisms
228
+ result["count"] = len(organisms)
229
+
230
+ return result
@@ -232,7 +232,7 @@ def get_logger(name: Optional[str] = None) -> logging.Logger:
232
232
  Args:
233
233
  name (str, optional): Logger name (usually __name__)
234
234
 
235
- Returns:
235
+ Returns
236
236
  logging.Logger: Logger instance
237
237
  """
238
238
  return _logger_manager.get_logger(name)
@@ -291,7 +291,7 @@ def get_hook_logger(name: str = "HookManager") -> logging.Logger:
291
291
  Args:
292
292
  name (str): Name of the logger. Defaults to 'HookManager'
293
293
 
294
- Returns:
294
+ Returns
295
295
  logging.Logger: Configured logger for hook operations
296
296
  """
297
297
  return get_logger(name)
@@ -8,11 +8,11 @@ supporting all MCP functionality including tools, resources, and prompts.
8
8
  import json
9
9
  import asyncio
10
10
  import websockets
11
- import aiohttp
12
- import uuid
13
11
  from typing import Dict, List, Any, Optional
14
12
  from urllib.parse import urljoin
15
13
  import warnings
14
+ from mcp.client.session import ClientSession
15
+ from mcp.client.streamable_http import streamablehttp_client
16
16
  from .base_tool import BaseTool
17
17
  from .tool_registry import register_tool
18
18
  import os
@@ -35,8 +35,6 @@ class BaseMCPClient:
35
35
  self.transport = normalized_transport
36
36
  self.timeout = timeout
37
37
  self.session = None
38
- self.mcp_session_id = None
39
- self._initialized = False
40
38
 
41
39
  # Validate transport (accept 'stdio' via normalization above)
42
40
  supported_transports = ["http", "websocket"]
@@ -44,24 +42,9 @@ class BaseMCPClient:
44
42
  # Keep message concise to satisfy line length rules
45
43
  raise ValueError("Invalid transport")
46
44
 
47
- async def _ensure_session(self):
48
- """Ensure HTTP session is available for HTTP transport"""
49
- if self.transport == "http" and self.session is None:
50
- connector = aiohttp.TCPConnector()
51
- timeout = aiohttp.ClientTimeout(total=self.timeout)
52
- self.session = aiohttp.ClientSession(connector=connector, timeout=timeout)
53
-
54
45
  async def _close_session(self):
55
- """Close HTTP session if exists"""
56
- if self.session:
57
- try:
58
- await self.session.close()
59
- except Exception:
60
- pass # Ignore errors during cleanup
61
- finally:
62
- self.session = None
63
- self.mcp_session_id = None
64
- self._initialized = False
46
+ """Placeholder for compatibility; HTTP client calls are scoped per request."""
47
+ return
65
48
 
66
49
  def _get_mcp_endpoint(self, path: str) -> str:
67
50
  """Get the full MCP endpoint URL"""
@@ -72,115 +55,55 @@ class BaseMCPClient:
72
55
  return urljoin(base_url + "/", path)
73
56
  return self.server_url
74
57
 
75
- async def _initialize_mcp_session(self):
76
- """Initialize MCP session if needed (for compatibility with different MCP servers)"""
77
- if self._initialized:
78
- return
79
-
80
- await self._ensure_session()
81
-
82
- # Try to get session ID from server
83
- try:
84
- url = f"{self.server_url.rstrip('/')}/mcp"
85
- test_payload = {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
86
-
87
- headers = {
88
- "Content-Type": "application/json",
89
- "Accept": "application/json, text/event-stream",
90
- }
91
-
92
- async with self.session.post(
93
- url, json=test_payload, headers=headers
94
- ) as response:
95
- session_id = response.headers.get("mcp-session-id")
96
- if session_id:
97
- self.mcp_session_id = session_id
98
-
99
- if response.status in [200, 400, 406, 500]:
100
- self._initialized = True
101
- return
102
-
103
- except Exception:
104
- pass
105
-
106
- # Fallback: generate session ID
107
- if not self.mcp_session_id:
108
- self.mcp_session_id = str(uuid.uuid4()).replace("-", "")
109
-
110
- self._initialized = True
111
-
112
58
  async def _make_mcp_request(
113
59
  self, method: str, params: Optional[Dict] = None
114
60
  ) -> Dict[str, Any]:
115
61
  """Make an MCP JSON-RPC request"""
116
- request_id = "1"
117
-
118
- payload = {"jsonrpc": "2.0", "id": request_id, "method": method}
119
-
120
- if params:
121
- payload["params"] = params
122
-
123
62
  if self.transport == "http":
124
- await self._ensure_session()
125
- await self._initialize_mcp_session() # Ensure session is initialized
126
-
127
- headers = {
128
- "Content-Type": "application/json",
129
- "Accept": "application/json, text/event-stream",
130
- }
131
-
132
- # Add session ID if available
133
- if self.mcp_session_id:
134
- headers["mcp-session-id"] = self.mcp_session_id
135
-
136
63
  endpoint = self._get_mcp_endpoint("")
137
-
138
- async with self.session.post(
139
- endpoint, json=payload, headers=headers
140
- ) as response:
141
- if response.status != 200:
142
- raise Exception(
143
- f"MCP request failed with status {response.status}: {await response.text()}"
144
- )
145
-
146
- content_type = response.headers.get("content-type", "").lower()
147
-
148
- if "text/event-stream" in content_type:
149
- # Handle Server-Sent Events format
150
- response_text = await response.text()
151
-
152
- for line in response_text.split("\n"):
153
- line = line.strip()
154
- if line.startswith("data: "):
155
- json_data = line[6:]
156
- try:
157
- result = json.loads(json_data)
158
- break
159
- except json.JSONDecodeError:
160
- continue
161
- else:
162
- raise Exception(
163
- f"Failed to parse SSE response: {response_text}"
64
+ async with streamablehttp_client(endpoint, timeout=self.timeout) as (
65
+ read_stream,
66
+ write_stream,
67
+ _,
68
+ ):
69
+ async with ClientSession(read_stream, write_stream) as session:
70
+ await session.initialize()
71
+
72
+ if method == "tools/list":
73
+ result = await session.list_tools()
74
+ elif method == "tools/call":
75
+ if not params or "name" not in params:
76
+ raise ValueError("Missing tool name for tools/call")
77
+ result = await session.call_tool(
78
+ params["name"], params.get("arguments") or {}
164
79
  )
165
-
166
- elif "application/json" in content_type:
167
- result = await response.json()
168
- else:
169
- try:
170
- result = await response.json()
171
- except Exception:
172
- response_text = await response.text()
173
- raise Exception(
174
- f"Unexpected content type {content_type}. Response: {response_text}"
80
+ elif method == "resources/list":
81
+ result = await session.list_resources()
82
+ elif method == "resources/read":
83
+ if not params or "uri" not in params:
84
+ raise ValueError("Missing uri for resources/read")
85
+ result = await session.read_resource(params["uri"])
86
+ elif method == "prompts/list":
87
+ result = await session.list_prompts()
88
+ elif method == "prompts/get":
89
+ if not params or "name" not in params:
90
+ raise ValueError("Missing prompt name for prompts/get")
91
+ result = await session.get_prompt(
92
+ params["name"], params.get("arguments")
175
93
  )
94
+ else:
95
+ raise ValueError(f"Unsupported MCP method: {method}")
176
96
 
177
- if "error" in result:
178
- raise Exception(f"MCP error: {result['error']}")
179
-
180
- return result.get("result", {})
97
+ if hasattr(result, "model_dump"):
98
+ return result.model_dump(mode="json")
99
+ return result
181
100
 
182
101
  elif self.transport == "websocket":
183
102
  async with websockets.connect(self.server_url) as websocket:
103
+ request_id = "1"
104
+ payload = {"jsonrpc": "2.0", "id": request_id, "method": method}
105
+ if params:
106
+ payload["params"] = params
184
107
  await websocket.send(json.dumps(payload))
185
108
  response = await websocket.recv()
186
109
  result = json.loads(response)
@@ -575,7 +498,6 @@ class MCPAutoLoaderTool(BaseTool, BaseMCPClient):
575
498
  async def discover_tools(self) -> Dict[str, Any]:
576
499
  """Discover all available tools from the MCP server"""
577
500
  try:
578
- await self._initialize_mcp_session()
579
501
  tools_response = await self._make_mcp_request("tools/list")
580
502
  tools = tools_response.get("tools", [])
581
503
 
@@ -631,22 +553,28 @@ class MCPAutoLoaderTool(BaseTool, BaseMCPClient):
631
553
  return configs
632
554
 
633
555
  def register_tools_in_engine(self, engine):
634
- """Register discovered tools directly in the ToolUniverse engine"""
556
+ """Register discovered tools using ToolUniverse public API"""
635
557
  try:
636
558
  configs = self.generate_proxy_tool_configs()
637
559
 
638
560
  for config in configs:
639
- # Add configuration to engine's all_tools list for validation
640
- engine.all_tools.append(config)
561
+ config["type"]
562
+ tool_name = config["name"]
641
563
 
642
- # Create MCPProxyTool instance for execution
643
- proxy_tool = MCPProxyTool(config)
564
+ # Use public API to register tool
565
+ engine.register_custom_tool(
566
+ tool_class=MCPProxyTool,
567
+ tool_name=tool_name,
568
+ tool_config=config,
569
+ instantiate=True, # Immediately instantiate and cache
570
+ )
644
571
 
645
- # Register both config (for validation) and tool instance (for execution)
646
- tool_name = config["name"]
647
- engine.all_tool_dict[tool_name] = config # For validation
648
- engine.callable_functions[tool_name] = proxy_tool # For execution
649
- self._registered_tools[tool_name] = proxy_tool
572
+ # Keep reference for tracking
573
+ # Use the actual key that register_custom_tool uses
574
+ actual_key = config.get("name", tool_name)
575
+ self._registered_tools[tool_name] = engine.callable_functions[
576
+ actual_key
577
+ ]
650
578
 
651
579
  return len(configs)
652
580
  except Exception as e: