camel-ai 0.2.72a4__py3-none-any.whl → 0.2.72a5__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 camel-ai might be problematic. Click here for more details.

@@ -33,6 +33,20 @@ if TYPE_CHECKING:
33
33
  logger = get_logger(__name__)
34
34
 
35
35
 
36
+ class TabIdGenerator:
37
+ """Monotonically increasing tab ID generator."""
38
+
39
+ _counter: int = 0
40
+ _lock: ClassVar[asyncio.Lock] = asyncio.Lock()
41
+
42
+ @classmethod
43
+ async def generate_tab_id(cls) -> str:
44
+ """Generate a monotonically increasing tab ID."""
45
+ async with cls._lock:
46
+ cls._counter += 1
47
+ return f"tab-{cls._counter:03d}"
48
+
49
+
36
50
  class HybridBrowserSession:
37
51
  """Lightweight wrapper around Playwright for
38
52
  browsing with multi-tab support.
@@ -172,9 +186,9 @@ class HybridBrowserSession:
172
186
  self._context: Optional[BrowserContext] = None
173
187
  self._page: Optional[Page] = None
174
188
 
175
- # Multi-tab support
176
- self._pages: List[Page] = [] # All tabs
177
- self._current_tab_index: int = 0 # Current active tab index
189
+ # Dictionary-based tab management with monotonic IDs
190
+ self._pages: Dict[str, Page] = {} # tab_id -> Page object
191
+ self._current_tab_id: Optional[str] = None # Current active tab ID
178
192
 
179
193
  self.snapshot: Optional[PageSnapshot] = None
180
194
  self.executor: Optional[ActionExecutor] = None
@@ -221,20 +235,23 @@ class HybridBrowserSession:
221
235
  # ------------------------------------------------------------------
222
236
  # Multi-tab management methods
223
237
  # ------------------------------------------------------------------
224
- async def create_new_tab(self, url: Optional[str] = None) -> int:
238
+ async def create_new_tab(self, url: Optional[str] = None) -> str:
225
239
  r"""Create a new tab and optionally navigate to a URL.
226
240
 
227
241
  Args:
228
242
  url: Optional URL to navigate to in the new tab
229
243
 
230
244
  Returns:
231
- int: Index of the newly created tab
245
+ str: ID of the newly created tab
232
246
  """
233
247
  await self.ensure_browser()
234
248
 
235
249
  if self._context is None:
236
250
  raise RuntimeError("Browser context is not available")
237
251
 
252
+ # Generate unique tab ID
253
+ tab_id = await TabIdGenerator.generate_tab_id()
254
+
238
255
  # Create new page
239
256
  new_page = await self._context.new_page()
240
257
 
@@ -248,9 +265,8 @@ class HybridBrowserSession:
248
265
  f"Failed to apply stealth script to new tab: {e}"
249
266
  )
250
267
 
251
- # Add to our pages list
252
- self._pages.append(new_page)
253
- new_tab_index = len(self._pages) - 1
268
+ # Store in pages dictionary
269
+ self._pages[tab_id] = new_page
254
270
 
255
271
  # Navigate if URL provided
256
272
  if url:
@@ -261,196 +277,168 @@ class HybridBrowserSession:
261
277
  logger.warning(f"Failed to navigate new tab to {url}: {e}")
262
278
 
263
279
  logger.info(
264
- f"Created new tab {new_tab_index}, total tabs: {len(self._pages)}"
280
+ f"Created new tab {tab_id}, total tabs: {len(self._pages)}"
265
281
  )
266
- return new_tab_index
282
+ return tab_id
267
283
 
268
- async def register_page(self, new_page: "Page") -> int:
284
+ async def register_page(self, new_page: "Page") -> str:
269
285
  r"""Register a page that was created externally (e.g., by a click).
270
286
 
271
287
  Args:
272
288
  new_page (Page): The new page object to register.
273
289
 
274
290
  Returns:
275
- int: The index of the (newly) registered tab.
291
+ str: The ID of the (newly) registered tab.
276
292
  """
277
- if new_page in self._pages:
278
- try:
279
- # Page is already known, just return its index
280
- return self._pages.index(new_page)
281
- except ValueError:
282
- # Should not happen if `in` check passed, but handle anyway
283
- pass
284
-
285
- # Add new page to our list
286
- self._pages.append(new_page)
287
- new_tab_index = len(self._pages) - 1
293
+ # Check if page is already registered
294
+ for tab_id, page in self._pages.items():
295
+ if page is new_page:
296
+ return tab_id
297
+
298
+ # Create new ID for the page
299
+ tab_id = await TabIdGenerator.generate_tab_id()
300
+ self._pages[tab_id] = new_page
301
+
288
302
  logger.info(
289
- f"Registered new tab {new_tab_index} (opened by user action). "
303
+ f"Registered new tab {tab_id} (opened by user action). "
290
304
  f"Total tabs: {len(self._pages)}"
291
305
  )
292
- return new_tab_index
306
+ return tab_id
293
307
 
294
- async def switch_to_tab(self, tab_index: int) -> bool:
295
- r"""Switch to a specific tab by index.
308
+ async def switch_to_tab(self, tab_id: str) -> bool:
309
+ r"""Switch to a specific tab by ID.
296
310
 
297
311
  Args:
298
- tab_index: Index of the tab to switch to
312
+ tab_id: ID of the tab to switch to
299
313
 
300
314
  Returns:
301
- bool: True if successful, False if tab index is invalid
315
+ bool: True if successful, False if tab ID is invalid
302
316
  """
303
- # Use a more robust bounds check to prevent race conditions
304
- try:
305
- if not self._pages:
306
- logger.warning("No tabs available")
307
- return False
317
+ if tab_id not in self._pages:
318
+ logger.warning(f"Invalid tab ID: {tab_id}")
319
+ return False
308
320
 
309
- # Capture current state to avoid race conditions
310
- current_pages = self._pages.copy()
311
- pages_count = len(current_pages)
321
+ page = self._pages[tab_id]
312
322
 
313
- if tab_index < 0 or tab_index >= pages_count:
314
- logger.warning(
315
- f"Invalid tab index {tab_index}, available "
316
- f"tabs: {pages_count}"
317
- )
318
- return False
323
+ # Check if page is still valid
324
+ if page.is_closed():
325
+ logger.warning(f"Tab {tab_id} is closed, removing from registry")
326
+ # Clean up closed tab
327
+ del self._pages[tab_id]
328
+ return False
319
329
 
320
- # Check if the page is still valid
321
- page = current_pages[tab_index]
322
- if page.is_closed():
323
- logger.warning(
324
- f"Tab {tab_index} is closed, removing from list"
325
- )
326
- # Remove closed page from original list
327
- if (
328
- tab_index < len(self._pages)
329
- and self._pages[tab_index] is page
330
- ):
331
- self._pages.pop(tab_index)
332
- # Adjust current tab index if necessary
333
- if self._current_tab_index >= len(self._pages):
334
- self._current_tab_index = max(0, len(self._pages) - 1)
335
- return False
336
-
337
- self._current_tab_index = tab_index
330
+ try:
331
+ # Switch to the tab
332
+ self._current_tab_id = tab_id
338
333
  self._page = page
339
334
 
340
335
  # Bring the tab to the front in the browser window
341
- await self._page.bring_to_front()
336
+ await page.bring_to_front()
342
337
 
343
- # Update executor and snapshot for new tab
338
+ # Update utilities for new tab
344
339
  self.executor = ActionExecutor(
345
- self._page,
340
+ page,
346
341
  self,
347
342
  default_timeout=self._default_timeout,
348
343
  short_timeout=self._short_timeout,
349
344
  )
350
- self.snapshot = PageSnapshot(self._page)
345
+ self.snapshot = PageSnapshot(page)
351
346
 
352
- logger.info(f"Switched to tab {tab_index}")
347
+ logger.info(f"Switched to tab {tab_id}")
353
348
  return True
354
349
 
355
350
  except Exception as e:
356
- logger.warning(f"Error switching to tab {tab_index}: {e}")
351
+ logger.warning(f"Error switching to tab {tab_id}: {e}")
357
352
  return False
358
353
 
359
- async def close_tab(self, tab_index: int) -> bool:
360
- r"""Close a specific tab.
354
+ async def close_tab(self, tab_id: str) -> bool:
355
+ r"""Close a specific tab by ID.
361
356
 
362
357
  Args:
363
- tab_index: Index of the tab to close
358
+ tab_id: ID of the tab to close
364
359
 
365
360
  Returns:
366
- bool: True if successful, False if tab index is invalid
361
+ bool: True if successful, False if tab ID is invalid
367
362
  """
368
- if not self._pages or tab_index < 0 or tab_index >= len(self._pages):
363
+ if tab_id not in self._pages:
364
+ logger.warning(f"Invalid tab ID: {tab_id}")
369
365
  return False
370
366
 
367
+ page = self._pages[tab_id]
368
+
371
369
  try:
372
- page = self._pages[tab_index]
370
+ # Close the page if not already closed
373
371
  if not page.is_closed():
374
372
  await page.close()
375
373
 
376
- # Remove from our list
377
- self._pages.pop(tab_index)
374
+ # Remove from our dictionary
375
+ del self._pages[tab_id]
378
376
 
379
377
  # If we closed the current tab, switch to another one
380
- if tab_index == self._current_tab_index:
378
+ if tab_id == self._current_tab_id:
381
379
  if self._pages:
382
- # Switch to the previous tab, or first tab if we closed
383
- # the first one
384
- new_index = max(
385
- 0, min(tab_index - 1, len(self._pages) - 1)
386
- )
387
- await self.switch_to_tab(new_index)
380
+ # Switch to any available tab (first one we find)
381
+ next_tab_id = next(iter(self._pages.keys()))
382
+ await self.switch_to_tab(next_tab_id)
388
383
  else:
389
384
  # No tabs left
390
- self._current_tab_index = 0
385
+ self._current_tab_id = None
391
386
  self._page = None
392
387
  self.executor = None
393
388
  self.snapshot = None
394
- elif tab_index < self._current_tab_index:
395
- # Adjust current tab index since we removed a tab before it
396
- self._current_tab_index -= 1
397
389
 
398
390
  logger.info(
399
- f"Closed tab {tab_index}, remaining tabs: {len(self._pages)}"
391
+ f"Closed tab {tab_id}, remaining tabs: {len(self._pages)}"
400
392
  )
401
393
  return True
402
394
 
403
395
  except Exception as e:
404
- logger.warning(f"Error closing tab {tab_index}: {e}")
396
+ logger.warning(f"Error closing tab {tab_id}: {e}")
405
397
  return False
406
398
 
407
399
  async def get_tab_info(self) -> List[Dict[str, Any]]:
408
- r"""Get information about all open tabs.
400
+ r"""Get information about all open tabs including IDs.
409
401
 
410
402
  Returns:
411
403
  List of dictionaries containing tab information
412
404
  """
413
405
  tab_info = []
414
- for i, page in enumerate(self._pages):
406
+ tabs_to_cleanup = []
407
+
408
+ # Process all tabs in dictionary
409
+ for tab_id, page in list(self._pages.items()):
415
410
  try:
416
411
  if not page.is_closed():
417
412
  title = await page.title()
418
413
  url = page.url
419
- is_current = i == self._current_tab_index
414
+ is_current = tab_id == self._current_tab_id
420
415
  tab_info.append(
421
416
  {
422
- "index": i,
417
+ "tab_id": tab_id,
423
418
  "title": title,
424
419
  "url": url,
425
420
  "is_current": is_current,
426
421
  }
427
422
  )
428
423
  else:
429
- # Mark closed tab for removal
430
- tab_info.append(
431
- {
432
- "index": i,
433
- "title": "[CLOSED]",
434
- "url": "",
435
- "is_current": False,
436
- }
437
- )
424
+ # Mark for cleanup
425
+ tabs_to_cleanup.append(tab_id)
438
426
  except Exception as e:
439
- logger.warning(f"Error getting info for tab {i}: {e}")
440
- tab_info.append(
441
- {
442
- "index": i,
443
- "title": "[ERROR]",
444
- "url": "",
445
- "is_current": False,
446
- }
447
- )
427
+ logger.warning(f"Error getting info for tab {tab_id}: {e}")
428
+ tabs_to_cleanup.append(tab_id)
429
+
430
+ # Clean up closed/invalid tabs
431
+ for tab_id in tabs_to_cleanup:
432
+ if tab_id in self._pages:
433
+ del self._pages[tab_id]
448
434
 
449
435
  return tab_info
450
436
 
451
- async def get_current_tab_index(self) -> int:
452
- r"""Get the index of the current active tab."""
453
- return self._current_tab_index
437
+ async def get_current_tab_id(self) -> Optional[str]:
438
+ r"""Get the id for the current active tab."""
439
+ if not self._current_tab_id or not self._pages:
440
+ return None
441
+ return self._current_tab_id
454
442
 
455
443
  # ------------------------------------------------------------------
456
444
  # Browser lifecycle helpers
@@ -470,7 +458,7 @@ class HybridBrowserSession:
470
458
  self._context = singleton_instance._context
471
459
  self._page = singleton_instance._page
472
460
  self._pages = singleton_instance._pages
473
- self._current_tab_index = singleton_instance._current_tab_index
461
+ self._current_tab_id = singleton_instance._current_tab_id
474
462
  self.snapshot = singleton_instance.snapshot
475
463
  self.executor = singleton_instance.executor
476
464
  return
@@ -512,17 +500,30 @@ class HybridBrowserSession:
512
500
  pages = context.pages
513
501
  if pages:
514
502
  self._page = pages[0]
515
- self._pages = list(pages)
503
+ # Create ID for initial page
504
+ initial_tab_id = await TabIdGenerator.generate_tab_id()
505
+ self._pages[initial_tab_id] = pages[0]
506
+ self._current_tab_id = initial_tab_id
507
+ # Handle additional pages if any
508
+ for page in pages[1:]:
509
+ tab_id = await TabIdGenerator.generate_tab_id()
510
+ self._pages[tab_id] = page
516
511
  else:
517
512
  self._page = await context.new_page()
518
- self._pages = [self._page]
513
+ initial_tab_id = await TabIdGenerator.generate_tab_id()
514
+ self._pages[initial_tab_id] = self._page
515
+ self._current_tab_id = initial_tab_id
519
516
  else:
520
517
  self._browser = await self._playwright.chromium.launch(
521
518
  **launch_options
522
519
  )
523
520
  self._context = await self._browser.new_context(**context_options)
524
521
  self._page = await self._context.new_page()
525
- self._pages = [self._page]
522
+
523
+ # Create ID for initial page
524
+ initial_tab_id = await TabIdGenerator.generate_tab_id()
525
+ self._pages[initial_tab_id] = self._page
526
+ self._current_tab_id = initial_tab_id
526
527
 
527
528
  # Apply stealth modifications if enabled
528
529
  if self._stealth and self._stealth_script:
@@ -544,7 +545,6 @@ class HybridBrowserSession:
544
545
  default_timeout=self._default_timeout,
545
546
  short_timeout=self._short_timeout,
546
547
  )
547
- self._current_tab_index = 0
548
548
 
549
549
  logger.info("Browser session initialized successfully")
550
550
 
@@ -593,8 +593,8 @@ class HybridBrowserSession:
593
593
  logger.error(f"Error during browser session close: {e}")
594
594
  finally:
595
595
  self._page = None
596
- self._pages = []
597
- self._current_tab_index = 0
596
+ self._pages = {}
597
+ self._current_tab_id = None
598
598
  self.snapshot = None
599
599
  self.executor = None
600
600
 
@@ -602,7 +602,7 @@ class HybridBrowserSession:
602
602
  r"""Internal session close logic with thorough cleanup."""
603
603
  try:
604
604
  # Close all pages first
605
- pages_to_close = self._pages.copy()
605
+ pages_to_close = list(self._pages.values())
606
606
  for page in pages_to_close:
607
607
  try:
608
608
  if not page.is_closed():
@@ -614,7 +614,7 @@ class HybridBrowserSession:
614
614
  except Exception as e:
615
615
  logger.warning(f"Error closing page: {e}")
616
616
 
617
- # Clear the pages list
617
+ # Clear the pages dictionary
618
618
  self._pages.clear()
619
619
 
620
620
  # Close context with explicit wait
@@ -658,7 +658,8 @@ class HybridBrowserSession:
658
658
  finally:
659
659
  # Ensure all attributes are cleared regardless of errors
660
660
  self._page = None
661
- self._pages = []
661
+ self._pages = {}
662
+ self._current_tab_id = None
662
663
  self._context = None
663
664
  self._browser = None
664
665
  self._playwright = None
@@ -766,7 +766,7 @@ class HybridBrowserToolkit(BaseToolkit):
766
766
  # Add debug info for tab info retrieval
767
767
  logger.debug("Attempting to get tab info from session...")
768
768
  tab_info = await session.get_tab_info()
769
- current_tab_index = await session.get_current_tab_index()
769
+ current_tab_index = await session.get_current_tab_id()
770
770
 
771
771
  # Debug log the successful retrieval
772
772
  logger.debug(
@@ -814,7 +814,7 @@ class HybridBrowserToolkit(BaseToolkit):
814
814
  try:
815
815
  open_pages = [
816
816
  p
817
- for p in fallback_session._pages
817
+ for p in fallback_session._pages.values()
818
818
  if not p.is_closed()
819
819
  ]
820
820
  actual_tab_count = len(open_pages)
@@ -1228,9 +1228,9 @@ class HybridBrowserToolkit(BaseToolkit):
1228
1228
  if should_create_new_tab:
1229
1229
  logger.info(f"Creating new tab and navigating to URL: {url}")
1230
1230
  try:
1231
- new_tab_index = await session.create_new_tab(url)
1232
- await session.switch_to_tab(new_tab_index)
1233
- nav_result = f"Visited {url} in new tab {new_tab_index}"
1231
+ new_tab_id = await session.create_new_tab(url)
1232
+ await session.switch_to_tab(new_tab_id)
1233
+ nav_result = f"Visited {url} in new tab {new_tab_id}"
1234
1234
  except Exception as e:
1235
1235
  logger.error(f"Failed to create new tab and navigate: {e}")
1236
1236
  nav_result = f"Error creating new tab: {e}"
@@ -1897,14 +1897,14 @@ class HybridBrowserToolkit(BaseToolkit):
1897
1897
  )
1898
1898
 
1899
1899
  @action_logger
1900
- async def switch_tab(self, *, tab_index: int) -> Dict[str, Any]:
1901
- r"""Switches to a different browser tab using its index.
1900
+ async def switch_tab(self, *, tab_id: str) -> Dict[str, Any]:
1901
+ r"""Switches to a different browser tab using its ID.
1902
1902
 
1903
1903
  After switching, all actions will apply to the new tab. Use
1904
- `get_tab_info` to find the index of the tab you want to switch to.
1904
+ `get_tab_info` to find the ID of the tab you want to switch to.
1905
1905
 
1906
1906
  Args:
1907
- tab_index (int): The zero-based index of the tab to activate.
1907
+ tab_id (str): The ID of the tab to activate.
1908
1908
 
1909
1909
  Returns:
1910
1910
  Dict[str, Any]: A dictionary with the result of the action:
@@ -1917,7 +1917,7 @@ class HybridBrowserToolkit(BaseToolkit):
1917
1917
  await self._ensure_browser()
1918
1918
  session = await self._get_session()
1919
1919
 
1920
- success = await session.switch_to_tab(tab_index)
1920
+ success = await session.switch_to_tab(tab_id)
1921
1921
 
1922
1922
  if success:
1923
1923
  snapshot = await session.get_snapshot(
@@ -1926,14 +1926,14 @@ class HybridBrowserToolkit(BaseToolkit):
1926
1926
  tab_info = await self._get_tab_info_for_output()
1927
1927
 
1928
1928
  result = {
1929
- "result": f"Successfully switched to tab {tab_index}",
1929
+ "result": f"Successfully switched to tab {tab_id}",
1930
1930
  "snapshot": snapshot,
1931
1931
  **tab_info,
1932
1932
  }
1933
1933
  else:
1934
1934
  tab_info = await self._get_tab_info_for_output()
1935
1935
  result = {
1936
- "result": f"Failed to switch to tab {tab_index}. Tab may not "
1936
+ "result": f"Failed to switch to tab {tab_id}. Tab may not "
1937
1937
  f"exist.",
1938
1938
  "snapshot": "",
1939
1939
  **tab_info,
@@ -1942,14 +1942,14 @@ class HybridBrowserToolkit(BaseToolkit):
1942
1942
  return result
1943
1943
 
1944
1944
  @action_logger
1945
- async def close_tab(self, *, tab_index: int) -> Dict[str, Any]:
1946
- r"""Closes a browser tab using its index.
1945
+ async def close_tab(self, *, tab_id: str) -> Dict[str, Any]:
1946
+ r"""Closes a browser tab using its ID.
1947
1947
 
1948
- Use `get_tab_info` to find the index of the tab to close. After
1948
+ Use `get_tab_info` to find the ID of the tab to close. After
1949
1949
  closing, the browser will switch to another tab if available.
1950
1950
 
1951
1951
  Args:
1952
- tab_index (int): The zero-based index of the tab to close.
1952
+ tab_id (str): The ID of the tab to close.
1953
1953
 
1954
1954
  Returns:
1955
1955
  Dict[str, Any]: A dictionary with the result of the action:
@@ -1962,7 +1962,7 @@ class HybridBrowserToolkit(BaseToolkit):
1962
1962
  await self._ensure_browser()
1963
1963
  session = await self._get_session()
1964
1964
 
1965
- success = await session.close_tab(tab_index)
1965
+ success = await session.close_tab(tab_id)
1966
1966
 
1967
1967
  if success:
1968
1968
  # Get current state after closing the tab
@@ -1976,14 +1976,14 @@ class HybridBrowserToolkit(BaseToolkit):
1976
1976
  tab_info = await self._get_tab_info_for_output()
1977
1977
 
1978
1978
  result = {
1979
- "result": f"Successfully closed tab {tab_index}",
1979
+ "result": f"Successfully closed tab {tab_id}",
1980
1980
  "snapshot": snapshot,
1981
1981
  **tab_info,
1982
1982
  }
1983
1983
  else:
1984
1984
  tab_info = await self._get_tab_info_for_output()
1985
1985
  result = {
1986
- "result": f"Failed to close tab {tab_index}. Tab may not "
1986
+ "result": f"Failed to close tab {tab_id}. Tab may not "
1987
1987
  f"exist.",
1988
1988
  "snapshot": "",
1989
1989
  **tab_info,
@@ -0,0 +1,95 @@
1
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
+
15
+ from typing import List, Optional
16
+
17
+ from camel.toolkits import BaseToolkit, FunctionTool, MCPToolkit
18
+
19
+
20
+ class OrigeneToolkit(BaseToolkit):
21
+ r"""OrigeneToolkit provides an interface for interacting with
22
+ Origene MCP server.
23
+
24
+ This toolkit can be used as an async context manager for automatic
25
+ connection management:
26
+
27
+ async with OrigeneToolkit() as toolkit:
28
+ tools = toolkit.get_tools()
29
+ # Toolkit is automatically disconnected when exiting
30
+
31
+ Attributes:
32
+ timeout (Optional[float]): Connection timeout in seconds.
33
+ (default: :obj:`None`)
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ timeout: Optional[float] = None,
39
+ ) -> None:
40
+ r"""Initializes the OrigeneToolkit.
41
+
42
+ Args:
43
+ timeout (Optional[float]): Connection timeout in seconds.
44
+ (default: :obj:`None`)
45
+ """
46
+ super().__init__(timeout=timeout)
47
+
48
+ self._mcp_toolkit = MCPToolkit(
49
+ config_dict={
50
+ "mcpServers": {
51
+ "pubchem_mcp": {
52
+ "url": "http://127.0.0.1:8791/mcp/",
53
+ "mode": "streamable-http",
54
+ }
55
+ }
56
+ },
57
+ timeout=timeout,
58
+ )
59
+
60
+ async def connect(self):
61
+ r"""Explicitly connect to the Origene MCP server."""
62
+ await self._mcp_toolkit.connect()
63
+
64
+ async def disconnect(self):
65
+ r"""Explicitly disconnect from the Origene MCP server."""
66
+ await self._mcp_toolkit.disconnect()
67
+
68
+ async def __aenter__(self) -> "OrigeneToolkit":
69
+ r"""Async context manager entry point.
70
+
71
+ Returns:
72
+ OrigeneToolkit: The connected toolkit instance.
73
+
74
+ Example:
75
+ async with OrigeneToolkit() as toolkit:
76
+ tools = toolkit.get_tools()
77
+ """
78
+ await self.connect()
79
+ return self
80
+
81
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
82
+ r"""Async context manager exit point.
83
+
84
+ Automatically disconnects from the Origene MCP server.
85
+ """
86
+ await self.disconnect()
87
+ return None
88
+
89
+ def get_tools(self) -> List[FunctionTool]:
90
+ r"""Returns a list of tools provided by the Origene MCP server.
91
+
92
+ Returns:
93
+ List[FunctionTool]: List of available tools.
94
+ """
95
+ return self._mcp_toolkit.get_tools()