tooluniverse 0.2.0__py3-none-any.whl → 1.0.0__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 (190) hide show
  1. tooluniverse/__init__.py +340 -4
  2. tooluniverse/admetai_tool.py +84 -0
  3. tooluniverse/agentic_tool.py +563 -0
  4. tooluniverse/alphafold_tool.py +96 -0
  5. tooluniverse/base_tool.py +129 -6
  6. tooluniverse/boltz_tool.py +207 -0
  7. tooluniverse/chem_tool.py +192 -0
  8. tooluniverse/compose_scripts/__init__.py +1 -0
  9. tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
  10. tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
  11. tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
  12. tooluniverse/compose_scripts/literature_tool.py +34 -0
  13. tooluniverse/compose_scripts/output_summarizer.py +279 -0
  14. tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
  15. tooluniverse/compose_scripts/tool_discover.py +705 -0
  16. tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
  17. tooluniverse/compose_tool.py +371 -0
  18. tooluniverse/ctg_tool.py +1002 -0
  19. tooluniverse/custom_tool.py +81 -0
  20. tooluniverse/dailymed_tool.py +108 -0
  21. tooluniverse/data/admetai_tools.json +155 -0
  22. tooluniverse/data/agentic_tools.json +1156 -0
  23. tooluniverse/data/alphafold_tools.json +87 -0
  24. tooluniverse/data/boltz_tools.json +9 -0
  25. tooluniverse/data/chembl_tools.json +16 -0
  26. tooluniverse/data/clait_tools.json +108 -0
  27. tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
  28. tooluniverse/data/compose_tools.json +202 -0
  29. tooluniverse/data/dailymed_tools.json +70 -0
  30. tooluniverse/data/dataset_tools.json +646 -0
  31. tooluniverse/data/disease_target_score_tools.json +712 -0
  32. tooluniverse/data/efo_tools.json +17 -0
  33. tooluniverse/data/embedding_tools.json +319 -0
  34. tooluniverse/data/enrichr_tools.json +31 -0
  35. tooluniverse/data/europe_pmc_tools.json +22 -0
  36. tooluniverse/data/expert_feedback_tools.json +10 -0
  37. tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
  38. tooluniverse/data/fda_drug_labeling_tools.json +1 -1
  39. tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
  40. tooluniverse/data/finder_tools.json +209 -0
  41. tooluniverse/data/gene_ontology_tools.json +113 -0
  42. tooluniverse/data/gwas_tools.json +1082 -0
  43. tooluniverse/data/hpa_tools.json +333 -0
  44. tooluniverse/data/humanbase_tools.json +47 -0
  45. tooluniverse/data/idmap_tools.json +74 -0
  46. tooluniverse/data/mcp_client_tools_example.json +113 -0
  47. tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
  48. tooluniverse/data/medlineplus_tools.json +141 -0
  49. tooluniverse/data/monarch_tools.json +1 -1
  50. tooluniverse/data/openalex_tools.json +36 -0
  51. tooluniverse/data/opentarget_tools.json +1 -1
  52. tooluniverse/data/output_summarization_tools.json +101 -0
  53. tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
  54. tooluniverse/data/packages/categorized_tools.txt +206 -0
  55. tooluniverse/data/packages/cheminformatics_tools.json +347 -0
  56. tooluniverse/data/packages/earth_sciences_tools.json +74 -0
  57. tooluniverse/data/packages/genomics_tools.json +776 -0
  58. tooluniverse/data/packages/image_processing_tools.json +38 -0
  59. tooluniverse/data/packages/machine_learning_tools.json +789 -0
  60. tooluniverse/data/packages/neuroscience_tools.json +62 -0
  61. tooluniverse/data/packages/original_tools.txt +0 -0
  62. tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
  63. tooluniverse/data/packages/scientific_computing_tools.json +560 -0
  64. tooluniverse/data/packages/single_cell_tools.json +453 -0
  65. tooluniverse/data/packages/software_tools.json +4954 -0
  66. tooluniverse/data/packages/structural_biology_tools.json +396 -0
  67. tooluniverse/data/packages/visualization_tools.json +399 -0
  68. tooluniverse/data/pubchem_tools.json +215 -0
  69. tooluniverse/data/pubtator_tools.json +68 -0
  70. tooluniverse/data/rcsb_pdb_tools.json +1332 -0
  71. tooluniverse/data/reactome_tools.json +19 -0
  72. tooluniverse/data/semantic_scholar_tools.json +26 -0
  73. tooluniverse/data/special_tools.json +2 -25
  74. tooluniverse/data/tool_composition_tools.json +88 -0
  75. tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
  76. tooluniverse/data/txagent_client_tools.json +9 -0
  77. tooluniverse/data/uniprot_tools.json +211 -0
  78. tooluniverse/data/url_fetch_tools.json +94 -0
  79. tooluniverse/data/uspto_downloader_tools.json +9 -0
  80. tooluniverse/data/uspto_tools.json +811 -0
  81. tooluniverse/data/xml_tools.json +3275 -0
  82. tooluniverse/dataset_tool.py +296 -0
  83. tooluniverse/default_config.py +165 -0
  84. tooluniverse/efo_tool.py +42 -0
  85. tooluniverse/embedding_database.py +630 -0
  86. tooluniverse/embedding_sync.py +396 -0
  87. tooluniverse/enrichr_tool.py +266 -0
  88. tooluniverse/europe_pmc_tool.py +52 -0
  89. tooluniverse/execute_function.py +1775 -95
  90. tooluniverse/extended_hooks.py +444 -0
  91. tooluniverse/gene_ontology_tool.py +194 -0
  92. tooluniverse/graphql_tool.py +158 -36
  93. tooluniverse/gwas_tool.py +358 -0
  94. tooluniverse/hpa_tool.py +1645 -0
  95. tooluniverse/humanbase_tool.py +389 -0
  96. tooluniverse/logging_config.py +254 -0
  97. tooluniverse/mcp_client_tool.py +764 -0
  98. tooluniverse/mcp_integration.py +413 -0
  99. tooluniverse/mcp_tool_registry.py +925 -0
  100. tooluniverse/medlineplus_tool.py +337 -0
  101. tooluniverse/openalex_tool.py +228 -0
  102. tooluniverse/openfda_adv_tool.py +283 -0
  103. tooluniverse/openfda_tool.py +393 -160
  104. tooluniverse/output_hook.py +1122 -0
  105. tooluniverse/package_tool.py +195 -0
  106. tooluniverse/pubchem_tool.py +158 -0
  107. tooluniverse/pubtator_tool.py +168 -0
  108. tooluniverse/rcsb_pdb_tool.py +38 -0
  109. tooluniverse/reactome_tool.py +108 -0
  110. tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
  111. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
  112. tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
  113. tooluniverse/remote/expert_feedback/simple_test.py +23 -0
  114. tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
  115. tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
  116. tooluniverse/remote/expert_feedback_mcp/human_expert_mcp_server.py +1611 -0
  117. tooluniverse/remote/expert_feedback_mcp/simple_test.py +34 -0
  118. tooluniverse/remote/expert_feedback_mcp/start_web_interface.py +91 -0
  119. tooluniverse/remote/immune_compass/compass_tool.py +327 -0
  120. tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
  121. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
  122. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
  123. tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
  124. tooluniverse/remote_tool.py +99 -0
  125. tooluniverse/restful_tool.py +53 -30
  126. tooluniverse/scripts/generate_tool_graph.py +408 -0
  127. tooluniverse/scripts/visualize_tool_graph.py +829 -0
  128. tooluniverse/semantic_scholar_tool.py +62 -0
  129. tooluniverse/smcp.py +2452 -0
  130. tooluniverse/smcp_server.py +975 -0
  131. tooluniverse/test/mcp_server_test.py +0 -0
  132. tooluniverse/test/test_admetai_tool.py +370 -0
  133. tooluniverse/test/test_agentic_tool.py +129 -0
  134. tooluniverse/test/test_alphafold_tool.py +71 -0
  135. tooluniverse/test/test_chem_tool.py +37 -0
  136. tooluniverse/test/test_compose_lieraturereview.py +63 -0
  137. tooluniverse/test/test_compose_tool.py +448 -0
  138. tooluniverse/test/test_dailymed.py +69 -0
  139. tooluniverse/test/test_dataset_tool.py +200 -0
  140. tooluniverse/test/test_disease_target_score.py +56 -0
  141. tooluniverse/test/test_drugbank_filter_examples.py +179 -0
  142. tooluniverse/test/test_efo.py +31 -0
  143. tooluniverse/test/test_enrichr_tool.py +21 -0
  144. tooluniverse/test/test_europe_pmc_tool.py +20 -0
  145. tooluniverse/test/test_fda_adv.py +95 -0
  146. tooluniverse/test/test_fda_drug_labeling.py +91 -0
  147. tooluniverse/test/test_gene_ontology_tools.py +66 -0
  148. tooluniverse/test/test_gwas_tool.py +139 -0
  149. tooluniverse/test/test_hpa.py +625 -0
  150. tooluniverse/test/test_humanbase_tool.py +20 -0
  151. tooluniverse/test/test_idmap_tools.py +61 -0
  152. tooluniverse/test/test_mcp_server.py +211 -0
  153. tooluniverse/test/test_mcp_tool.py +247 -0
  154. tooluniverse/test/test_medlineplus.py +220 -0
  155. tooluniverse/test/test_openalex_tool.py +32 -0
  156. tooluniverse/test/test_opentargets.py +28 -0
  157. tooluniverse/test/test_pubchem_tool.py +116 -0
  158. tooluniverse/test/test_pubtator_tool.py +37 -0
  159. tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
  160. tooluniverse/test/test_reactome.py +54 -0
  161. tooluniverse/test/test_semantic_scholar_tool.py +24 -0
  162. tooluniverse/test/test_software_tools.py +147 -0
  163. tooluniverse/test/test_tool_description_optimizer.py +49 -0
  164. tooluniverse/test/test_tool_finder.py +26 -0
  165. tooluniverse/test/test_tool_finder_llm.py +252 -0
  166. tooluniverse/test/test_tools_find.py +195 -0
  167. tooluniverse/test/test_uniprot_tools.py +74 -0
  168. tooluniverse/test/test_uspto_tool.py +72 -0
  169. tooluniverse/test/test_xml_tool.py +113 -0
  170. tooluniverse/tool_finder_embedding.py +267 -0
  171. tooluniverse/tool_finder_keyword.py +693 -0
  172. tooluniverse/tool_finder_llm.py +699 -0
  173. tooluniverse/tool_graph_web_ui.py +955 -0
  174. tooluniverse/tool_registry.py +416 -0
  175. tooluniverse/uniprot_tool.py +155 -0
  176. tooluniverse/url_tool.py +253 -0
  177. tooluniverse/uspto_tool.py +240 -0
  178. tooluniverse/utils.py +369 -41
  179. tooluniverse/xml_tool.py +369 -0
  180. tooluniverse-1.0.0.dist-info/METADATA +377 -0
  181. tooluniverse-1.0.0.dist-info/RECORD +186 -0
  182. tooluniverse-1.0.0.dist-info/entry_points.txt +9 -0
  183. tooluniverse/generate_mcp_tools.py +0 -113
  184. tooluniverse/mcp_server.py +0 -3340
  185. tooluniverse-0.2.0.dist-info/METADATA +0 -139
  186. tooluniverse-0.2.0.dist-info/RECORD +0 -21
  187. tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
  188. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/WHEEL +0 -0
  189. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/licenses/LICENSE +0 -0
  190. {tooluniverse-0.2.0.dist-info → tooluniverse-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,764 @@
1
+ """
2
+ MCP Client Tool for ToolUniverse
3
+
4
+ This module provides a tool that acts as a client to connect to an existing MCP server,
5
+ supporting all MCP functionality including tools, resources, and prompts.
6
+ """
7
+
8
+ import json
9
+ import asyncio
10
+ import websockets
11
+ import aiohttp
12
+ import uuid
13
+ from typing import Dict, List, Any, Optional
14
+ from urllib.parse import urljoin
15
+ import warnings
16
+ from .base_tool import BaseTool
17
+ from .tool_registry import register_tool
18
+ import os
19
+
20
+
21
+ class BaseMCPClient:
22
+ """
23
+ Base MCP client with common functionality shared between MCPClientTool and MCPAutoLoaderTool.
24
+ Provides session management, request handling, and async cleanup patterns.
25
+ """
26
+
27
+ def __init__(self, server_url: str, transport: str = "http", timeout: int = 30):
28
+ self.server_url = os.path.expandvars(server_url)
29
+ self.transport = transport
30
+ self.timeout = timeout
31
+ self.session = None
32
+ self.mcp_session_id = None
33
+ self._initialized = False
34
+
35
+ # Validate transport
36
+ supported_transports = ["http", "websocket"]
37
+ if self.transport not in supported_transports:
38
+ raise ValueError(
39
+ f"Invalid transport '{self.transport}'. Supported: {supported_transports}"
40
+ )
41
+
42
+ async def _ensure_session(self):
43
+ """Ensure HTTP session is available for HTTP transport"""
44
+ if self.transport == "http" and self.session is None:
45
+ connector = aiohttp.TCPConnector()
46
+ timeout = aiohttp.ClientTimeout(total=self.timeout)
47
+ self.session = aiohttp.ClientSession(connector=connector, timeout=timeout)
48
+
49
+ async def _close_session(self):
50
+ """Close HTTP session if exists"""
51
+ if self.session:
52
+ try:
53
+ await self.session.close()
54
+ except Exception:
55
+ pass # Ignore errors during cleanup
56
+ finally:
57
+ self.session = None
58
+ self.mcp_session_id = None
59
+ self._initialized = False
60
+
61
+ def _get_mcp_endpoint(self, path: str) -> str:
62
+ """Get the full MCP endpoint URL"""
63
+ if self.transport == "http":
64
+ base_url = self.server_url.rstrip("/")
65
+ if not base_url.endswith("/mcp"):
66
+ base_url += "/mcp"
67
+ return urljoin(base_url + "/", path)
68
+ return self.server_url
69
+
70
+ async def _initialize_mcp_session(self):
71
+ """Initialize MCP session if needed (for compatibility with different MCP servers)"""
72
+ if self._initialized:
73
+ return
74
+
75
+ await self._ensure_session()
76
+
77
+ # Try to get session ID from server
78
+ try:
79
+ url = f"{self.server_url.rstrip('/')}/mcp"
80
+ test_payload = {"jsonrpc": "2.0", "id": 1, "method": "tools/list"}
81
+
82
+ headers = {
83
+ "Content-Type": "application/json",
84
+ "Accept": "application/json, text/event-stream",
85
+ }
86
+
87
+ async with self.session.post(
88
+ url, json=test_payload, headers=headers
89
+ ) as response:
90
+ session_id = response.headers.get("mcp-session-id")
91
+ if session_id:
92
+ self.mcp_session_id = session_id
93
+
94
+ if response.status in [200, 400, 406, 500]:
95
+ self._initialized = True
96
+ return
97
+
98
+ except Exception:
99
+ pass
100
+
101
+ # Fallback: generate session ID
102
+ if not self.mcp_session_id:
103
+ self.mcp_session_id = str(uuid.uuid4()).replace("-", "")
104
+
105
+ self._initialized = True
106
+
107
+ async def _make_mcp_request(
108
+ self, method: str, params: Optional[Dict] = None
109
+ ) -> Dict[str, Any]:
110
+ """Make an MCP JSON-RPC request"""
111
+ request_id = "1"
112
+
113
+ payload = {"jsonrpc": "2.0", "id": request_id, "method": method}
114
+
115
+ if params:
116
+ payload["params"] = params
117
+
118
+ if self.transport == "http":
119
+ await self._ensure_session()
120
+ await self._initialize_mcp_session() # Ensure session is initialized
121
+
122
+ headers = {
123
+ "Content-Type": "application/json",
124
+ "Accept": "application/json, text/event-stream",
125
+ }
126
+
127
+ # Add session ID if available
128
+ if self.mcp_session_id:
129
+ headers["mcp-session-id"] = self.mcp_session_id
130
+
131
+ endpoint = self._get_mcp_endpoint("")
132
+
133
+ async with self.session.post(
134
+ endpoint, json=payload, headers=headers
135
+ ) as response:
136
+ if response.status != 200:
137
+ raise Exception(
138
+ f"MCP request failed with status {response.status}: {await response.text()}"
139
+ )
140
+
141
+ content_type = response.headers.get("content-type", "").lower()
142
+
143
+ if "text/event-stream" in content_type:
144
+ # Handle Server-Sent Events format
145
+ response_text = await response.text()
146
+
147
+ for line in response_text.split("\n"):
148
+ line = line.strip()
149
+ if line.startswith("data: "):
150
+ json_data = line[6:]
151
+ try:
152
+ result = json.loads(json_data)
153
+ break
154
+ except json.JSONDecodeError:
155
+ continue
156
+ else:
157
+ raise Exception(
158
+ f"Failed to parse SSE response: {response_text}"
159
+ )
160
+
161
+ elif "application/json" in content_type:
162
+ result = await response.json()
163
+ else:
164
+ try:
165
+ result = await response.json()
166
+ except Exception:
167
+ response_text = await response.text()
168
+ raise Exception(
169
+ f"Unexpected content type {content_type}. Response: {response_text}"
170
+ )
171
+
172
+ if "error" in result:
173
+ raise Exception(f"MCP error: {result['error']}")
174
+
175
+ return result.get("result", {})
176
+
177
+ elif self.transport == "websocket":
178
+ async with websockets.connect(self.server_url) as websocket:
179
+ await websocket.send(json.dumps(payload))
180
+ response = await websocket.recv()
181
+ result = json.loads(response)
182
+
183
+ if "error" in result:
184
+ raise Exception(f"MCP error: {result['error']}")
185
+
186
+ return result.get("result", {})
187
+
188
+ else:
189
+ raise ValueError(f"Unsupported transport: {self.transport}")
190
+
191
+ def _run_with_cleanup(self, async_func):
192
+ """Common async execution pattern with proper cleanup"""
193
+ try:
194
+ loop = asyncio.new_event_loop()
195
+ asyncio.set_event_loop(loop)
196
+ return loop.run_until_complete(async_func())
197
+ finally:
198
+ try:
199
+ pending = asyncio.all_tasks(loop)
200
+ for task in pending:
201
+ task.cancel()
202
+ if pending:
203
+ loop.run_until_complete(
204
+ asyncio.gather(*pending, return_exceptions=True)
205
+ )
206
+ loop.close()
207
+ except Exception:
208
+ pass
209
+
210
+
211
+ @register_tool("MCPClientTool")
212
+ class MCPClientTool(BaseTool, BaseMCPClient):
213
+ """
214
+ A tool that acts as an MCP client to connect to existing MCP servers.
215
+ Supports both HTTP and WebSocket transports.
216
+ """
217
+
218
+ def __init__(self, tool_config):
219
+ BaseTool.__init__(self, tool_config)
220
+ BaseMCPClient.__init__(
221
+ self,
222
+ server_url=tool_config.get("server_url", "http://localhost:8000"),
223
+ transport=tool_config.get("transport", "http"),
224
+ timeout=tool_config.get("timeout", 600),
225
+ )
226
+
227
+ # Debug logging for transport configuration
228
+ tool_name = tool_config.get("name", "Unknown")
229
+ print(
230
+ f"MCP Tool Init: {tool_name} -> transport={self.transport}, server={self.server_url}"
231
+ )
232
+
233
+ self._tools_cache = None
234
+ self._resources_cache = None
235
+ self._prompts_cache = None
236
+
237
+ async def list_tools(self) -> List[Dict[str, Any]]:
238
+ """List available tools from the MCP server"""
239
+ if self._tools_cache is None:
240
+ result = await self._make_mcp_request("tools/list")
241
+ self._tools_cache = result.get("tools", [])
242
+ return self._tools_cache
243
+
244
+ async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
245
+ """Call a tool on the MCP server"""
246
+ params = {"name": name, "arguments": arguments}
247
+
248
+ result = await self._make_mcp_request("tools/call", params)
249
+ return result
250
+
251
+ async def list_resources(self) -> List[Dict[str, Any]]:
252
+ """List available resources from the MCP server"""
253
+ if self._resources_cache is None:
254
+ try:
255
+ result = await self._make_mcp_request("resources/list")
256
+ self._resources_cache = result.get("resources", [])
257
+ except Exception:
258
+ # Some servers might not support resources
259
+ self._resources_cache = []
260
+ return self._resources_cache
261
+
262
+ async def read_resource(self, uri: str) -> Dict[str, Any]:
263
+ """Read a resource from the MCP server"""
264
+ params = {"uri": uri}
265
+ result = await self._make_mcp_request("resources/read", params)
266
+ return result
267
+
268
+ async def list_prompts(self) -> List[Dict[str, Any]]:
269
+ """List available prompts from the MCP server"""
270
+ if self._prompts_cache is None:
271
+ try:
272
+ result = await self._make_mcp_request("prompts/list")
273
+ self._prompts_cache = result.get("prompts", [])
274
+ except Exception:
275
+ # Some servers might not support prompts
276
+ self._prompts_cache = []
277
+ return self._prompts_cache
278
+
279
+ async def get_prompt(
280
+ self, name: str, arguments: Optional[Dict[str, Any]] = None
281
+ ) -> Dict[str, Any]:
282
+ """Get a prompt from the MCP server"""
283
+ params = {"name": name}
284
+ if arguments:
285
+ params["arguments"] = arguments
286
+
287
+ result = await self._make_mcp_request("prompts/get", params)
288
+ return result
289
+
290
+ def run(self, arguments):
291
+ """
292
+ Main run method for the tool.
293
+ Supports different operations based on the 'operation' argument.
294
+ """
295
+ operation = arguments.get("operation", "call_tool")
296
+
297
+ async def _run_async():
298
+ try:
299
+ if operation == "list_tools":
300
+ return await self._run_list_tools()
301
+ elif operation == "call_tool":
302
+ return await self._run_call_tool(arguments)
303
+ elif operation == "list_resources":
304
+ return await self._run_list_resources()
305
+ elif operation == "read_resource":
306
+ return await self._run_read_resource(arguments)
307
+ elif operation == "list_prompts":
308
+ return await self._run_list_prompts()
309
+ elif operation == "get_prompt":
310
+ return await self._run_get_prompt(arguments)
311
+ else:
312
+ return {"error": f"Unknown operation: {operation}"}
313
+ except Exception as e:
314
+ return {"error": str(e)}
315
+ finally:
316
+ # Always clean up session
317
+ await self._close_session()
318
+
319
+ return self._run_with_cleanup(_run_async)
320
+
321
+ async def _run_list_tools(self):
322
+ """Run list_tools operation"""
323
+ tools = await self.list_tools()
324
+ return {"tools": tools}
325
+
326
+ async def _run_call_tool(self, arguments):
327
+ """Run call_tool operation"""
328
+ tool_name = arguments.get("tool_name")
329
+ tool_arguments = arguments.get("tool_arguments", {})
330
+
331
+ if not tool_name:
332
+ return {"error": "tool_name is required for call_tool operation"}
333
+
334
+ result = await self.call_tool(tool_name, tool_arguments)
335
+ return result
336
+
337
+ async def _run_list_resources(self):
338
+ """Run list_resources operation"""
339
+ resources = await self.list_resources()
340
+ return {"resources": resources}
341
+
342
+ async def _run_read_resource(self, arguments):
343
+ """Run read_resource operation"""
344
+ uri = arguments.get("uri")
345
+
346
+ if not uri:
347
+ return {"error": "uri is required for read_resource operation"}
348
+
349
+ result = await self.read_resource(uri)
350
+ return result
351
+
352
+ async def _run_list_prompts(self):
353
+ """Run list_prompts operation"""
354
+ prompts = await self.list_prompts()
355
+ return {"prompts": prompts}
356
+
357
+ async def _run_get_prompt(self, arguments):
358
+ """Run get_prompt operation"""
359
+ prompt_name = arguments.get("prompt_name")
360
+ prompt_arguments = arguments.get("prompt_arguments", {})
361
+
362
+ if not prompt_name:
363
+ return {"error": "prompt_name is required for get_prompt operation"}
364
+
365
+ result = await self.get_prompt(prompt_name, prompt_arguments)
366
+ return result
367
+
368
+
369
+ @register_tool("MCPProxyTool")
370
+ class MCPProxyTool(MCPClientTool):
371
+ """
372
+ A proxy tool that automatically forwards tool calls to an MCP server.
373
+ This creates individual tools for each tool available on the MCP server.
374
+ """
375
+
376
+ def __init__(self, tool_config):
377
+ super().__init__(tool_config)
378
+ self.target_tool_name = tool_config.get("target_tool_name")
379
+ if not self.target_tool_name:
380
+ raise ValueError("MCPProxyTool requires 'target_tool_name' in tool_config")
381
+
382
+ def run(self, arguments):
383
+ """Forward the call directly to the target tool on the MCP server"""
384
+
385
+ async def _run_async():
386
+ try:
387
+ result = await self.call_tool(self.target_tool_name, arguments)
388
+ return result
389
+ except Exception as e:
390
+ return {"error": str(e)}
391
+ finally:
392
+ # Always clean up session
393
+ await self._close_session()
394
+
395
+ return self._run_with_cleanup(_run_async)
396
+
397
+
398
+ @register_tool("MCPServerDiscovery")
399
+ class MCPServerDiscovery:
400
+ """
401
+ Helper class to discover and create tool configurations for MCP servers.
402
+ """
403
+
404
+ @staticmethod
405
+ async def discover_server_tools(
406
+ server_url: str, transport: str = "http"
407
+ ) -> List[Dict[str, Any]]:
408
+ """
409
+ Discover all tools available on an MCP server and return tool configurations.
410
+ """
411
+ # Create a temporary client to discover tools
412
+ temp_config = {
413
+ "server_url": server_url,
414
+ "transport": transport,
415
+ "name": "temp_discovery",
416
+ "description": "Temporary tool for discovery",
417
+ }
418
+
419
+ client = MCPClientTool(temp_config)
420
+
421
+ try:
422
+ # Get available tools
423
+ tools = await client.list_tools()
424
+
425
+ # Create tool configurations for each discovered tool
426
+ tool_configs = []
427
+
428
+ for tool in tools:
429
+ tool_name = tool.get("name", "unknown_tool")
430
+ tool_description = tool.get(
431
+ "description", f"Tool {tool_name} from MCP server"
432
+ )
433
+
434
+ # Create a configuration for this specific tool
435
+ config = {
436
+ "name": f"mcp_{tool_name}",
437
+ "description": tool_description,
438
+ "type": "MCPProxyTool",
439
+ "server_url": server_url,
440
+ "transport": transport,
441
+ "target_tool_name": tool_name,
442
+ "parameter": {
443
+ "type": "object",
444
+ "properties": tool.get("inputSchema", {}).get("properties", {}),
445
+ "required": tool.get("inputSchema", {}).get("required", []),
446
+ },
447
+ }
448
+
449
+ tool_configs.append(config)
450
+
451
+ return tool_configs
452
+
453
+ except Exception as e:
454
+ print(f"Error discovering tools from MCP server {server_url}: {e}")
455
+ return []
456
+ finally:
457
+ await client._close_session()
458
+
459
+ @staticmethod
460
+ def create_mcp_tools_config(
461
+ server_configs: List[Dict[str, str]],
462
+ ) -> List[Dict[str, Any]]:
463
+ """
464
+ Create tool configurations for multiple MCP servers.
465
+
466
+ Args:
467
+ server_configs: List of server configurations, each containing:
468
+ - server_url: URL of the MCP server
469
+ - transport: 'http' or 'websocket' (optional, defaults to 'http')
470
+ - server_name: Name prefix for tools (optional)
471
+
472
+ Returns:
473
+ List of tool configurations that can be loaded into ToolUniverse
474
+ """
475
+ all_configs = []
476
+
477
+ for server_config in server_configs:
478
+ server_url = server_config["server_url"]
479
+ transport = server_config.get("transport", "http")
480
+ server_name = server_config.get("server_name", "mcp_server")
481
+
482
+ # Create a generic MCP client tool for this server
483
+ client_config = {
484
+ "name": f"{server_name}_client",
485
+ "description": f"MCP client for server at {server_url}",
486
+ "type": "MCPClientTool",
487
+ "server_url": server_url,
488
+ "transport": transport,
489
+ "parameter": {
490
+ "type": "object",
491
+ "properties": {
492
+ "operation": {
493
+ "type": "string",
494
+ "enum": [
495
+ "list_tools",
496
+ "call_tool",
497
+ "list_resources",
498
+ "read_resource",
499
+ "list_prompts",
500
+ "get_prompt",
501
+ ],
502
+ "description": "The MCP operation to perform",
503
+ },
504
+ "tool_name": {
505
+ "type": "string",
506
+ "description": "Name of the tool to call (required for call_tool operation)",
507
+ },
508
+ "tool_arguments": {
509
+ "type": "object",
510
+ "description": "Arguments to pass to the tool (for call_tool operation)",
511
+ },
512
+ "uri": {
513
+ "type": "string",
514
+ "description": "Resource URI (required for read_resource operation)",
515
+ },
516
+ "prompt_name": {
517
+ "type": "string",
518
+ "description": "Name of the prompt to get (required for get_prompt operation)",
519
+ },
520
+ "prompt_arguments": {
521
+ "type": "object",
522
+ "description": "Arguments to pass to the prompt (for get_prompt operation)",
523
+ },
524
+ },
525
+ "required": ["operation"],
526
+ },
527
+ }
528
+
529
+ all_configs.append(client_config)
530
+
531
+ return all_configs
532
+
533
+
534
+ @register_tool("MCPAutoLoaderTool")
535
+ class MCPAutoLoaderTool(BaseTool, BaseMCPClient):
536
+ """
537
+ An advanced MCP tool that automatically discovers and loads all tools from an MCP server.
538
+ It can register discovered tools as individual ToolUniverse tools for seamless usage.
539
+ """
540
+
541
+ def __init__(self, tool_config):
542
+ BaseTool.__init__(self, tool_config)
543
+ BaseMCPClient.__init__(
544
+ self,
545
+ server_url=tool_config.get("server_url", "http://localhost:8000"),
546
+ transport=tool_config.get("transport", "http"),
547
+ timeout=tool_config.get("timeout", 5),
548
+ )
549
+
550
+ self.auto_register = tool_config.get("auto_register", True)
551
+ self.tool_prefix = tool_config.get("tool_prefix", "mcp_")
552
+ self.selected_tools = tool_config.get(
553
+ "selected_tools", None
554
+ ) # None means load all
555
+
556
+ # Debug logging
557
+ print(
558
+ f"MCPAutoLoaderTool '{tool_config.get('name', 'Unknown')}' initialized with:"
559
+ )
560
+ print(f" - server_url: {self.server_url}")
561
+ print(f" - transport: {self.transport}")
562
+ print(f" - auto_register: {self.auto_register}")
563
+ print(f" - tool_prefix: {self.tool_prefix}")
564
+ print(f" - selected_tools: {self.selected_tools}")
565
+ print(f" - timeout: {self.timeout}")
566
+
567
+ self._discovered_tools = {}
568
+ self._registered_tools = {}
569
+
570
+ async def discover_tools(self) -> Dict[str, Any]:
571
+ """Discover all available tools from the MCP server"""
572
+ try:
573
+ await self._initialize_mcp_session()
574
+ tools_response = await self._make_mcp_request("tools/list")
575
+ tools = tools_response.get("tools", [])
576
+
577
+ self._discovered_tools = {}
578
+ for tool in tools:
579
+ tool_name = tool.get("name")
580
+ if tool_name:
581
+ self._discovered_tools[tool_name] = tool
582
+
583
+ return self._discovered_tools
584
+ except Exception as e:
585
+ raise Exception(f"Failed to discover tools: {str(e)}")
586
+
587
+ async def call_tool(
588
+ self, tool_name: str, arguments: Dict[str, Any]
589
+ ) -> Dict[str, Any]:
590
+ """Directly call an MCP tool by name"""
591
+ try:
592
+ params = {"name": tool_name, "arguments": arguments}
593
+
594
+ result = await self._make_mcp_request("tools/call", params)
595
+ return result
596
+ except Exception as e:
597
+ raise Exception(f"Failed to call tool {tool_name}: {str(e)}")
598
+
599
+ def generate_proxy_tool_configs(self) -> List[Dict[str, Any]]:
600
+ """Generate proxy tool configurations for discovered tools"""
601
+ configs = []
602
+
603
+ for tool_name, tool_info in self._discovered_tools.items():
604
+ # Skip if selected_tools is specified and this tool is not in it
605
+ if self.selected_tools and tool_name not in self.selected_tools:
606
+ continue
607
+
608
+ proxy_name = f"{self.tool_prefix}{tool_name}"
609
+
610
+ config = {
611
+ "name": proxy_name,
612
+ "description": tool_info.get(
613
+ "description", f"Auto-loaded MCP tool: {tool_name}"
614
+ ),
615
+ "type": "MCPProxyTool",
616
+ "server_url": self.server_url,
617
+ "transport": self.transport,
618
+ "target_tool_name": tool_name,
619
+ "parameter": tool_info.get(
620
+ "inputSchema", {"type": "object", "properties": {}, "required": []}
621
+ ),
622
+ }
623
+
624
+ configs.append(config)
625
+
626
+ return configs
627
+
628
+ def register_tools_in_engine(self, engine):
629
+ """Register discovered tools directly in the ToolUniverse engine"""
630
+ try:
631
+ configs = self.generate_proxy_tool_configs()
632
+
633
+ for config in configs:
634
+ # Add configuration to engine's all_tools list for validation
635
+ engine.all_tools.append(config)
636
+
637
+ # Create MCPProxyTool instance for execution
638
+ proxy_tool = MCPProxyTool(config)
639
+
640
+ # Register both config (for validation) and tool instance (for execution)
641
+ tool_name = config["name"]
642
+ engine.all_tool_dict[tool_name] = config # For validation
643
+ engine.callable_functions[tool_name] = proxy_tool # For execution
644
+ self._registered_tools[tool_name] = proxy_tool
645
+
646
+ return len(configs)
647
+ except Exception as e:
648
+ raise Exception(f"Failed to register tools: {str(e)}")
649
+
650
+ async def auto_load_and_register(self, engine) -> Dict[str, Any]:
651
+ """Automatically discover, load and register all MCP tools"""
652
+ try:
653
+ # Discover tools
654
+ discovered = await self.discover_tools()
655
+
656
+ print(
657
+ f"🔍 MCPAutoLoaderTool discovered {len(discovered)} tools from MCP server:"
658
+ )
659
+ for tool_name, tool_info in discovered.items():
660
+ print(
661
+ f" 📋 {tool_name}: {tool_info.get('description', 'No description')}"
662
+ )
663
+
664
+ # Register tools if auto_register is enabled
665
+ if self.auto_register:
666
+ registered_count = self.register_tools_in_engine(engine)
667
+
668
+ print(
669
+ f"✅ MCPAutoLoaderTool registered {registered_count} tools with prefix '{self.tool_prefix}':"
670
+ )
671
+ for registered_name in self._registered_tools.keys():
672
+ print(f" 🔧 {registered_name}")
673
+
674
+ return {
675
+ "discovered_count": len(discovered),
676
+ "registered_count": registered_count,
677
+ "tools": list(discovered.keys()),
678
+ "registered_tools": list(self._registered_tools.keys()),
679
+ }
680
+ else:
681
+ print(
682
+ "â„šī¸ MCPAutoLoaderTool auto_register is disabled. Tools not registered automatically."
683
+ )
684
+ return {
685
+ "discovered_count": len(discovered),
686
+ "tools": list(discovered.keys()),
687
+ "configs": self.generate_proxy_tool_configs(),
688
+ }
689
+ except Exception as e:
690
+ print(f"❌ MCPAutoLoaderTool auto-load failed: {str(e)}")
691
+ raise Exception(f"Auto-load failed: {str(e)}")
692
+
693
+ def run(self, arguments):
694
+ """Main run method for the auto-loader tool"""
695
+ operation = arguments.get("operation")
696
+
697
+ async def _run_async():
698
+ try:
699
+ if operation == "discover":
700
+ # Discover available tools
701
+ discovered = await self.discover_tools()
702
+ return {
703
+ "discovered_count": len(discovered),
704
+ "tools": list(discovered.keys()),
705
+ "tool_details": discovered,
706
+ }
707
+
708
+ elif operation == "generate_configs":
709
+ # Generate proxy tool configurations
710
+ if not self._discovered_tools:
711
+ # Need to discover first
712
+ await self.discover_tools()
713
+
714
+ configs = self.generate_proxy_tool_configs()
715
+ return {"configs": configs, "count": len(configs)}
716
+
717
+ elif operation == "call_tool":
718
+ # Directly call an MCP tool
719
+ tool_name = arguments.get("tool_name")
720
+ tool_arguments = arguments.get("tool_arguments", {})
721
+
722
+ if not tool_name:
723
+ raise ValueError(
724
+ "tool_name is required for call_tool operation"
725
+ )
726
+
727
+ result = await self.call_tool(tool_name, tool_arguments)
728
+ return result
729
+
730
+ else:
731
+ raise ValueError(f"Unsupported operation: {operation}")
732
+ finally:
733
+ # Always clean up session
734
+ await self._close_session()
735
+
736
+ return self._run_with_cleanup(_run_async)
737
+
738
+ def __del__(self):
739
+ """Cleanup when object is destroyed"""
740
+ if (
741
+ hasattr(self, "session")
742
+ and self.session
743
+ and hasattr(self.session, "_connector")
744
+ ):
745
+ # Suppress ResourceWarnings during cleanup
746
+ with warnings.catch_warnings():
747
+ warnings.simplefilter("ignore", ResourceWarning)
748
+ try:
749
+ # Try to get the current loop
750
+ try:
751
+ loop = asyncio.get_running_loop()
752
+ # Schedule cleanup in the current loop
753
+ loop.create_task(self._close_session())
754
+ except RuntimeError:
755
+ # No running loop, create a new one for cleanup
756
+ loop = asyncio.new_event_loop()
757
+ asyncio.set_event_loop(loop)
758
+ try:
759
+ loop.run_until_complete(self._close_session())
760
+ finally:
761
+ loop.close()
762
+ except Exception:
763
+ # If all else fails, just set session to None
764
+ self.session = None