tooluniverse 1.0.9__py3-none-any.whl → 1.0.10__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 (57) hide show
  1. tooluniverse/admetai_tool.py +1 -1
  2. tooluniverse/agentic_tool.py +65 -17
  3. tooluniverse/base_tool.py +19 -8
  4. tooluniverse/boltz_tool.py +1 -1
  5. tooluniverse/cache/result_cache_manager.py +167 -12
  6. tooluniverse/compose_scripts/drug_safety_analyzer.py +1 -1
  7. tooluniverse/compose_scripts/multi_agent_literature_search.py +1 -1
  8. tooluniverse/compose_scripts/output_summarizer.py +4 -4
  9. tooluniverse/compose_scripts/tool_graph_composer.py +1 -1
  10. tooluniverse/compose_scripts/tool_metadata_generator.py +1 -1
  11. tooluniverse/compose_tool.py +9 -9
  12. tooluniverse/core_tool.py +2 -2
  13. tooluniverse/ctg_tool.py +4 -4
  14. tooluniverse/custom_tool.py +1 -1
  15. tooluniverse/dataset_tool.py +2 -2
  16. tooluniverse/default_config.py +1 -1
  17. tooluniverse/enrichr_tool.py +14 -14
  18. tooluniverse/execute_function.py +520 -15
  19. tooluniverse/extended_hooks.py +4 -4
  20. tooluniverse/gene_ontology_tool.py +1 -1
  21. tooluniverse/generate_tools.py +3 -3
  22. tooluniverse/humanbase_tool.py +10 -10
  23. tooluniverse/logging_config.py +2 -2
  24. tooluniverse/mcp_client_tool.py +57 -129
  25. tooluniverse/mcp_integration.py +52 -49
  26. tooluniverse/mcp_tool_registry.py +147 -528
  27. tooluniverse/openalex_tool.py +8 -8
  28. tooluniverse/openfda_tool.py +2 -2
  29. tooluniverse/output_hook.py +15 -15
  30. tooluniverse/package_tool.py +1 -1
  31. tooluniverse/pmc_tool.py +2 -2
  32. tooluniverse/remote/boltz/boltz_mcp_server.py +1 -1
  33. tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +2 -2
  34. tooluniverse/remote/immune_compass/compass_tool.py +3 -3
  35. tooluniverse/remote/pinnacle/pinnacle_tool.py +2 -2
  36. tooluniverse/remote/transcriptformer/transcriptformer_tool.py +3 -3
  37. tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +3 -3
  38. tooluniverse/remote_tool.py +4 -4
  39. tooluniverse/scripts/filter_tool_files.py +2 -2
  40. tooluniverse/smcp.py +93 -12
  41. tooluniverse/smcp_server.py +100 -20
  42. tooluniverse/space/__init__.py +46 -0
  43. tooluniverse/space/loader.py +133 -0
  44. tooluniverse/space/validator.py +353 -0
  45. tooluniverse/tool_finder_embedding.py +2 -2
  46. tooluniverse/tool_finder_keyword.py +9 -9
  47. tooluniverse/tool_finder_llm.py +6 -6
  48. tooluniverse/tools/_shared_client.py +3 -3
  49. tooluniverse/url_tool.py +1 -1
  50. tooluniverse/uspto_tool.py +1 -1
  51. tooluniverse/utils.py +10 -10
  52. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/METADATA +7 -3
  53. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/RECORD +57 -54
  54. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/WHEEL +0 -0
  55. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/entry_points.txt +0 -0
  56. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.dist-info}/licenses/LICENSE +0 -0
  57. {tooluniverse-1.0.9.dist-info → tooluniverse-1.0.10.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 = []
@@ -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:
@@ -16,8 +16,8 @@ def load_mcp_tools(self, server_urls: List[str] = None, **kwargs):
16
16
  This method automatically discovers tools from MCP servers and registers them
17
17
  as ToolUniverse tools, enabling seamless usage of remote capabilities.
18
18
 
19
- Parameters:
20
- ===========
19
+ Parameters
20
+ ----------
21
21
  server_urls : list of str, optional
22
22
  List of MCP server URLs to load tools from. Examples:
23
23
 
@@ -35,31 +35,34 @@ def load_mcp_tools(self, server_urls: List[str] = None, **kwargs):
35
35
  - selected_tools (list): Specific tools to load from each server
36
36
  - categories (list): Tool categories to filter by
37
37
 
38
- Returns:
39
- ========
38
+ Returns
39
+ -------
40
40
  dict
41
41
  Summary of loaded tools with counts and any errors encountered.
42
42
 
43
- Examples:
44
- =========
43
+ Examples
44
+ --------
45
45
 
46
46
  Load from specific servers:
47
- ```python
48
- tu = ToolUniverse()
49
47
 
50
- # Load tools from multiple MCP servers
51
- result = tu.load_mcp_tools([
52
- "http://localhost:8001", # Local analysis server
53
- "http://ml-server:8002", # Remote ML server
54
- "ws://realtime:9000" # WebSocket server
55
- ])
48
+ .. code-block:: python
56
49
 
57
- print(f"Loaded {result['total_tools']} tools from {result['servers_connected']} servers")
58
- ```
50
+ tu = ToolUniverse()
51
+
52
+ # Load tools from multiple MCP servers
53
+ result = tu.load_mcp_tools([
54
+ "http://localhost:8001", # Local analysis server
55
+ "http://ml-server:8002", # Remote ML server
56
+ "ws://realtime:9000" # WebSocket server
57
+ ])
58
+
59
+ print(f"Loaded {result['total_tools']} tools from {result['servers_connected']} servers")
59
60
 
60
61
  Load with custom configuration:
61
- ```python
62
- tu.load_mcp_tools(
62
+
63
+ .. code-block:: python
64
+
65
+ tu.load_mcp_tools(
63
66
  server_urls=["http://localhost:8001"],
64
67
  tool_prefix="analysis\\_",
65
68
  timeout=60,
@@ -223,8 +226,8 @@ def discover_mcp_tools(self, server_urls: List[str] = None, **kwargs) -> Dict[st
223
226
  without actually registering them in ToolUniverse. Useful for exploration
224
227
  and selective tool loading.
225
228
 
226
- Parameters:
227
- ===========
229
+ Parameters
230
+ ----------
228
231
  server_urls : list of str, optional
229
232
  List of MCP server URLs to discover from
230
233
  **kwargs
@@ -232,28 +235,28 @@ def discover_mcp_tools(self, server_urls: List[str] = None, **kwargs) -> Dict[st
232
235
  - timeout (int): Connection timeout (default: 30)
233
236
  - include_schemas (bool): Include tool parameter schemas (default: True)
234
237
 
235
- Returns:
236
- ========
238
+ Returns
239
+ -------
237
240
  dict
238
241
  Discovery results with tools organized by server
239
242
 
240
- Examples:
241
- =========
242
- ```python
243
- tu = ToolUniverse()
244
-
245
- # Discover what's available
246
- discovery = tu.discover_mcp_tools([
247
- "http://localhost:8001",
248
- "http://ml-server:8002"
249
- ])
250
-
251
- # Show available tools
252
- for server, info in discovery["servers"].items():
253
- print(f"\\n{server}:")
254
- for tool in info.get("tools", []):
255
- print(f" - {tool['name']}: {tool['description']}")
256
- ```
243
+ Examples
244
+ --------
245
+ .. code-block:: python
246
+
247
+ tu = ToolUniverse()
248
+
249
+ # Discover what's available
250
+ discovery = tu.discover_mcp_tools([
251
+ "http://localhost:8001",
252
+ "http://ml-server:8002"
253
+ ])
254
+
255
+ # Show available tools
256
+ for server, info in discovery["servers"].items():
257
+ print(f"\\n{server}:")
258
+ for tool in info.get("tools", []):
259
+ print(f" - {tool['name']}: {tool['description']}")
257
260
  """
258
261
  if server_urls is None:
259
262
  try:
@@ -334,20 +337,20 @@ def list_mcp_connections(self) -> Dict[str, Any]:
334
337
  """
335
338
  List all active MCP connections and loaded tools.
336
339
 
337
- Returns:
338
- ========
340
+ Returns
341
+ -------
339
342
  dict
340
343
  Information about MCP connections, auto-loaders, and loaded tools
341
344
 
342
- Examples:
343
- =========
344
- ```python
345
- tu = ToolUniverse()
346
- tu.load_mcp_tools(["http://localhost:8001"])
345
+ Examples
346
+ --------
347
+ .. code-block:: python
347
348
 
348
- connections = tu.list_mcp_connections()
349
- print(f"Active MCP connections: {len(connections['connections'])}")
350
- ```
349
+ tu = ToolUniverse()
350
+ tu.load_mcp_tools(["http://localhost:8001"])
351
+
352
+ connections = tu.list_mcp_connections()
353
+ print(f"Active MCP connections: {len(connections['connections'])}")
351
354
  """
352
355
  mcp_tools = {}
353
356
  mcp_loaders = {}