tooluniverse 0.2.0__py3-none-any.whl â 1.0.1__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.
- tooluniverse/__init__.py +340 -4
- tooluniverse/admetai_tool.py +84 -0
- tooluniverse/agentic_tool.py +563 -0
- tooluniverse/alphafold_tool.py +96 -0
- tooluniverse/base_tool.py +129 -6
- tooluniverse/boltz_tool.py +207 -0
- tooluniverse/chem_tool.py +192 -0
- tooluniverse/compose_scripts/__init__.py +1 -0
- tooluniverse/compose_scripts/biomarker_discovery.py +293 -0
- tooluniverse/compose_scripts/comprehensive_drug_discovery.py +186 -0
- tooluniverse/compose_scripts/drug_safety_analyzer.py +89 -0
- tooluniverse/compose_scripts/literature_tool.py +34 -0
- tooluniverse/compose_scripts/output_summarizer.py +279 -0
- tooluniverse/compose_scripts/tool_description_optimizer.py +681 -0
- tooluniverse/compose_scripts/tool_discover.py +705 -0
- tooluniverse/compose_scripts/tool_graph_composer.py +448 -0
- tooluniverse/compose_tool.py +371 -0
- tooluniverse/ctg_tool.py +1002 -0
- tooluniverse/custom_tool.py +81 -0
- tooluniverse/dailymed_tool.py +108 -0
- tooluniverse/data/admetai_tools.json +155 -0
- tooluniverse/data/adverse_event_tools.json +108 -0
- tooluniverse/data/agentic_tools.json +1156 -0
- tooluniverse/data/alphafold_tools.json +87 -0
- tooluniverse/data/boltz_tools.json +9 -0
- tooluniverse/data/chembl_tools.json +16 -0
- tooluniverse/data/clinicaltrials_gov_tools.json +326 -0
- tooluniverse/data/compose_tools.json +202 -0
- tooluniverse/data/dailymed_tools.json +70 -0
- tooluniverse/data/dataset_tools.json +646 -0
- tooluniverse/data/disease_target_score_tools.json +712 -0
- tooluniverse/data/efo_tools.json +17 -0
- tooluniverse/data/embedding_tools.json +319 -0
- tooluniverse/data/enrichr_tools.json +31 -0
- tooluniverse/data/europe_pmc_tools.json +22 -0
- tooluniverse/data/expert_feedback_tools.json +10 -0
- tooluniverse/data/fda_drug_adverse_event_tools.json +491 -0
- tooluniverse/data/fda_drug_labeling_tools.json +1 -1
- tooluniverse/data/fda_drugs_with_brand_generic_names_for_tool.py +76929 -148860
- tooluniverse/data/finder_tools.json +209 -0
- tooluniverse/data/gene_ontology_tools.json +113 -0
- tooluniverse/data/gwas_tools.json +1082 -0
- tooluniverse/data/hpa_tools.json +333 -0
- tooluniverse/data/humanbase_tools.json +47 -0
- tooluniverse/data/idmap_tools.json +74 -0
- tooluniverse/data/mcp_client_tools_example.json +113 -0
- tooluniverse/data/mcpautoloadertool_defaults.json +28 -0
- tooluniverse/data/medlineplus_tools.json +141 -0
- tooluniverse/data/monarch_tools.json +1 -1
- tooluniverse/data/openalex_tools.json +36 -0
- tooluniverse/data/opentarget_tools.json +1 -1
- tooluniverse/data/output_summarization_tools.json +101 -0
- tooluniverse/data/packages/bioinformatics_core_tools.json +1756 -0
- tooluniverse/data/packages/categorized_tools.txt +206 -0
- tooluniverse/data/packages/cheminformatics_tools.json +347 -0
- tooluniverse/data/packages/earth_sciences_tools.json +74 -0
- tooluniverse/data/packages/genomics_tools.json +776 -0
- tooluniverse/data/packages/image_processing_tools.json +38 -0
- tooluniverse/data/packages/machine_learning_tools.json +789 -0
- tooluniverse/data/packages/neuroscience_tools.json +62 -0
- tooluniverse/data/packages/original_tools.txt +0 -0
- tooluniverse/data/packages/physics_astronomy_tools.json +62 -0
- tooluniverse/data/packages/scientific_computing_tools.json +560 -0
- tooluniverse/data/packages/single_cell_tools.json +453 -0
- tooluniverse/data/packages/structural_biology_tools.json +396 -0
- tooluniverse/data/packages/visualization_tools.json +399 -0
- tooluniverse/data/pubchem_tools.json +215 -0
- tooluniverse/data/pubtator_tools.json +68 -0
- tooluniverse/data/rcsb_pdb_tools.json +1332 -0
- tooluniverse/data/reactome_tools.json +19 -0
- tooluniverse/data/semantic_scholar_tools.json +26 -0
- tooluniverse/data/special_tools.json +2 -25
- tooluniverse/data/tool_composition_tools.json +88 -0
- tooluniverse/data/toolfinderkeyword_defaults.json +34 -0
- tooluniverse/data/txagent_client_tools.json +9 -0
- tooluniverse/data/uniprot_tools.json +211 -0
- tooluniverse/data/url_fetch_tools.json +94 -0
- tooluniverse/data/uspto_downloader_tools.json +9 -0
- tooluniverse/data/uspto_tools.json +811 -0
- tooluniverse/data/xml_tools.json +3275 -0
- tooluniverse/dataset_tool.py +296 -0
- tooluniverse/default_config.py +165 -0
- tooluniverse/efo_tool.py +42 -0
- tooluniverse/embedding_database.py +630 -0
- tooluniverse/embedding_sync.py +396 -0
- tooluniverse/enrichr_tool.py +266 -0
- tooluniverse/europe_pmc_tool.py +52 -0
- tooluniverse/execute_function.py +1775 -95
- tooluniverse/extended_hooks.py +444 -0
- tooluniverse/gene_ontology_tool.py +194 -0
- tooluniverse/graphql_tool.py +158 -36
- tooluniverse/gwas_tool.py +358 -0
- tooluniverse/hpa_tool.py +1645 -0
- tooluniverse/humanbase_tool.py +389 -0
- tooluniverse/logging_config.py +254 -0
- tooluniverse/mcp_client_tool.py +764 -0
- tooluniverse/mcp_integration.py +413 -0
- tooluniverse/mcp_tool_registry.py +925 -0
- tooluniverse/medlineplus_tool.py +337 -0
- tooluniverse/openalex_tool.py +228 -0
- tooluniverse/openfda_adv_tool.py +283 -0
- tooluniverse/openfda_tool.py +393 -160
- tooluniverse/output_hook.py +1122 -0
- tooluniverse/package_tool.py +195 -0
- tooluniverse/pubchem_tool.py +158 -0
- tooluniverse/pubtator_tool.py +168 -0
- tooluniverse/rcsb_pdb_tool.py +38 -0
- tooluniverse/reactome_tool.py +108 -0
- tooluniverse/remote/boltz/boltz_mcp_server.py +50 -0
- tooluniverse/remote/depmap_24q2/depmap_24q2_mcp_tool.py +442 -0
- tooluniverse/remote/expert_feedback/human_expert_mcp_tools.py +2013 -0
- tooluniverse/remote/expert_feedback/simple_test.py +23 -0
- tooluniverse/remote/expert_feedback/start_web_interface.py +188 -0
- tooluniverse/remote/expert_feedback/web_only_interface.py +0 -0
- tooluniverse/remote/immune_compass/compass_tool.py +327 -0
- tooluniverse/remote/pinnacle/pinnacle_tool.py +328 -0
- tooluniverse/remote/transcriptformer/transcriptformer_tool.py +586 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_mcp_server.py +61 -0
- tooluniverse/remote/uspto_downloader/uspto_downloader_tool.py +120 -0
- tooluniverse/remote_tool.py +99 -0
- tooluniverse/restful_tool.py +53 -30
- tooluniverse/scripts/generate_tool_graph.py +408 -0
- tooluniverse/scripts/visualize_tool_graph.py +829 -0
- tooluniverse/semantic_scholar_tool.py +62 -0
- tooluniverse/smcp.py +2452 -0
- tooluniverse/smcp_server.py +975 -0
- tooluniverse/test/mcp_server_test.py +0 -0
- tooluniverse/test/test_admetai_tool.py +370 -0
- tooluniverse/test/test_agentic_tool.py +129 -0
- tooluniverse/test/test_alphafold_tool.py +71 -0
- tooluniverse/test/test_chem_tool.py +37 -0
- tooluniverse/test/test_compose_lieraturereview.py +63 -0
- tooluniverse/test/test_compose_tool.py +448 -0
- tooluniverse/test/test_dailymed.py +69 -0
- tooluniverse/test/test_dataset_tool.py +200 -0
- tooluniverse/test/test_disease_target_score.py +56 -0
- tooluniverse/test/test_drugbank_filter_examples.py +179 -0
- tooluniverse/test/test_efo.py +31 -0
- tooluniverse/test/test_enrichr_tool.py +21 -0
- tooluniverse/test/test_europe_pmc_tool.py +20 -0
- tooluniverse/test/test_fda_adv.py +95 -0
- tooluniverse/test/test_fda_drug_labeling.py +91 -0
- tooluniverse/test/test_gene_ontology_tools.py +66 -0
- tooluniverse/test/test_gwas_tool.py +139 -0
- tooluniverse/test/test_hpa.py +625 -0
- tooluniverse/test/test_humanbase_tool.py +20 -0
- tooluniverse/test/test_idmap_tools.py +61 -0
- tooluniverse/test/test_mcp_server.py +211 -0
- tooluniverse/test/test_mcp_tool.py +247 -0
- tooluniverse/test/test_medlineplus.py +220 -0
- tooluniverse/test/test_openalex_tool.py +32 -0
- tooluniverse/test/test_opentargets.py +28 -0
- tooluniverse/test/test_pubchem_tool.py +116 -0
- tooluniverse/test/test_pubtator_tool.py +37 -0
- tooluniverse/test/test_rcsb_pdb_tool.py +86 -0
- tooluniverse/test/test_reactome.py +54 -0
- tooluniverse/test/test_semantic_scholar_tool.py +24 -0
- tooluniverse/test/test_software_tools.py +147 -0
- tooluniverse/test/test_tool_description_optimizer.py +49 -0
- tooluniverse/test/test_tool_finder.py +26 -0
- tooluniverse/test/test_tool_finder_llm.py +252 -0
- tooluniverse/test/test_tools_find.py +195 -0
- tooluniverse/test/test_uniprot_tools.py +74 -0
- tooluniverse/test/test_uspto_tool.py +72 -0
- tooluniverse/test/test_xml_tool.py +113 -0
- tooluniverse/tool_finder_embedding.py +267 -0
- tooluniverse/tool_finder_keyword.py +693 -0
- tooluniverse/tool_finder_llm.py +699 -0
- tooluniverse/tool_graph_web_ui.py +955 -0
- tooluniverse/tool_registry.py +416 -0
- tooluniverse/uniprot_tool.py +155 -0
- tooluniverse/url_tool.py +253 -0
- tooluniverse/uspto_tool.py +240 -0
- tooluniverse/utils.py +369 -41
- tooluniverse/xml_tool.py +369 -0
- tooluniverse-1.0.1.dist-info/METADATA +387 -0
- tooluniverse-1.0.1.dist-info/RECORD +182 -0
- tooluniverse-1.0.1.dist-info/entry_points.txt +9 -0
- tooluniverse/generate_mcp_tools.py +0 -113
- tooluniverse/mcp_server.py +0 -3340
- tooluniverse-0.2.0.dist-info/METADATA +0 -139
- tooluniverse-0.2.0.dist-info/RECORD +0 -21
- tooluniverse-0.2.0.dist-info/entry_points.txt +0 -4
- {tooluniverse-0.2.0.dist-info â tooluniverse-1.0.1.dist-info}/WHEEL +0 -0
- {tooluniverse-0.2.0.dist-info â tooluniverse-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-0.2.0.dist-info â tooluniverse-1.0.1.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
|