pomera-ai-commander 1.2.4 → 1.2.7

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.
@@ -56,6 +56,12 @@ logging.basicConfig(
56
56
  )
57
57
  logger = logging.getLogger(__name__)
58
58
 
59
+ # Import version from unified version module
60
+ try:
61
+ from pomera.version import __version__
62
+ except ImportError:
63
+ __version__ = "unknown"
64
+
59
65
 
60
66
  def main():
61
67
  """Main entry point for the Pomera MCP server."""
@@ -70,7 +76,7 @@ def main():
70
76
  parser.add_argument(
71
77
  "--version",
72
78
  action="version",
73
- version="pomera-mcp-server 1.2.4"
79
+ version=f"pomera-mcp-server {__version__}"
74
80
  )
75
81
  parser.add_argument(
76
82
  "--list-tools",
@@ -160,7 +166,7 @@ def main():
160
166
  server = StdioMCPServer(
161
167
  tool_registry=registry,
162
168
  server_name="pomera-mcp-server",
163
- server_version="1.2.4"
169
+ server_version=__version__
164
170
  )
165
171
 
166
172
  logger.info("Starting Pomera MCP Server...")
@@ -46,33 +46,39 @@ class CurlHistoryManager:
46
46
  Manages request history persistence and organization for the cURL Tool.
47
47
 
48
48
  Handles:
49
- - History file storage and loading
49
+ - History file storage and loading (JSON or database backend)
50
50
  - History item management (add, remove, search)
51
51
  - History cleanup and organization
52
52
  - Collections support
53
53
  """
54
54
 
55
55
  def __init__(self, history_file: str = "settings.json",
56
- max_items: int = 100, logger=None):
56
+ max_items: int = 100, logger=None, db_settings_manager=None):
57
57
  """
58
58
  Initialize the history manager.
59
59
 
60
60
  Args:
61
- history_file: Path to the main settings file
61
+ history_file: Path to the main settings file (used if no db_settings_manager)
62
62
  max_items: Maximum number of history items to keep
63
63
  logger: Logger instance for debugging
64
+ db_settings_manager: DatabaseSettingsManager instance for database backend (optional)
64
65
  """
65
66
  self.history_file = history_file
66
67
  self.max_items = max_items
67
68
  self.logger = logger or logging.getLogger(__name__)
68
69
  self.tool_key = "cURL Tool" # Key in tool_settings section
69
70
 
71
+ # Database backend support
72
+ self.db_settings_manager = db_settings_manager
73
+ self.use_database = db_settings_manager is not None
74
+
70
75
  # History storage
71
76
  self.history: List[RequestHistoryItem] = []
72
77
  self.collections: Dict[str, List[str]] = {} # Collection name -> list of history item IDs
73
78
 
74
79
  # Load history on initialization
75
80
  self.load_history()
81
+
76
82
 
77
83
  def add_request(self, method: str, url: str, headers: Dict[str, str] = None,
78
84
  body: str = None, auth_type: str = "None",
@@ -505,12 +511,34 @@ class CurlHistoryManager:
505
511
 
506
512
  def load_history(self) -> bool:
507
513
  """
508
- Load history from centralized settings.json file.
514
+ Load history from database or settings.json file.
509
515
 
510
516
  Returns:
511
517
  True if successful
512
518
  """
513
519
  try:
520
+ if self.use_database and self.db_settings_manager:
521
+ # Load from database backend
522
+ curl_settings = self.db_settings_manager.get_tool_settings(self.tool_key)
523
+ if curl_settings:
524
+ # Load history items
525
+ self.history = []
526
+ for item_data in curl_settings.get("history", []):
527
+ try:
528
+ item = RequestHistoryItem.from_dict(item_data)
529
+ self.history.append(item)
530
+ except Exception as e:
531
+ self.logger.warning(f"Error loading history item: {e}")
532
+
533
+ # Load collections
534
+ self.collections = curl_settings.get("collections", {})
535
+
536
+ self.logger.info(f"Loaded {len(self.history)} history items from database")
537
+ else:
538
+ self.logger.info("No history found in database, starting with empty history")
539
+ return True
540
+
541
+ # Fallback to JSON file
514
542
  if os.path.exists(self.history_file):
515
543
  with open(self.history_file, 'r', encoding='utf-8') as f:
516
544
  all_settings = json.load(f)
@@ -543,12 +571,24 @@ class CurlHistoryManager:
543
571
 
544
572
  def save_history(self) -> bool:
545
573
  """
546
- Save history to centralized settings.json file.
574
+ Save history to database or settings.json file.
547
575
 
548
576
  Returns:
549
577
  True if successful
550
578
  """
551
579
  try:
580
+ if self.use_database and self.db_settings_manager:
581
+ # Save to database backend
582
+ history_data = [item.to_dict() for item in self.history]
583
+ self.db_settings_manager.set_tool_setting(self.tool_key, "history", history_data)
584
+ self.db_settings_manager.set_tool_setting(self.tool_key, "collections", self.collections)
585
+ self.db_settings_manager.set_tool_setting(self.tool_key, "history_last_updated", datetime.now().isoformat())
586
+ self.db_settings_manager.set_tool_setting(self.tool_key, "history_version", "1.0")
587
+
588
+ self.logger.debug("History saved to database")
589
+ return True
590
+
591
+ # Fallback to JSON file
552
592
  # Load existing settings file
553
593
  all_settings = {}
554
594
  if os.path.exists(self.history_file):
@@ -60,6 +60,17 @@ except ImportError:
60
60
  CURL_HISTORY_AVAILABLE = False
61
61
  print("cURL modules not available")
62
62
 
63
+ # Import database-compatible settings manager
64
+ try:
65
+ from core.database_curl_settings_manager import DatabaseCurlSettingsManager
66
+ DATABASE_CURL_SETTINGS_AVAILABLE = True
67
+ except ImportError:
68
+ try:
69
+ from ..core.database_curl_settings_manager import DatabaseCurlSettingsManager
70
+ DATABASE_CURL_SETTINGS_AVAILABLE = True
71
+ except ImportError:
72
+ DATABASE_CURL_SETTINGS_AVAILABLE = False
73
+
63
74
 
64
75
  def get_system_encryption_key():
65
76
  """Generate encryption key based on system characteristics (same as AI Tools)"""
@@ -138,7 +149,7 @@ class CurlToolWidget:
138
149
  with the application's tool ecosystem.
139
150
  """
140
151
 
141
- def __init__(self, parent, logger=None, send_to_input_callback=None, dialog_manager=None):
152
+ def __init__(self, parent, logger=None, send_to_input_callback=None, dialog_manager=None, db_settings_manager=None):
142
153
  """
143
154
  Initialize the cURL Tool widget.
144
155
 
@@ -147,21 +158,30 @@ class CurlToolWidget:
147
158
  logger: Logger instance for debugging
148
159
  send_to_input_callback: Callback function to send content to input tabs
149
160
  dialog_manager: DialogManager instance for configurable dialogs
161
+ db_settings_manager: DatabaseSettingsManager instance for database backend (optional)
150
162
  """
151
163
  self.parent = parent
152
164
  self.logger = logger or logging.getLogger(__name__)
153
165
  self.send_to_input_callback = send_to_input_callback
154
166
  self.dialog_manager = dialog_manager
167
+ self.db_settings_manager = db_settings_manager # Store for database backend
155
168
 
156
- # Initialize processor and settings manager
169
+ # Initialize processor
157
170
  if CURL_PROCESSOR_AVAILABLE:
158
171
  self.processor = CurlProcessor()
159
172
  else:
160
173
  self.processor = None
161
174
  self.logger.error("cURL Processor not available")
162
175
 
163
- if CURL_SETTINGS_AVAILABLE:
176
+ # Initialize settings manager - prefer database backend if available
177
+ if db_settings_manager and DATABASE_CURL_SETTINGS_AVAILABLE:
178
+ # Use database-backed settings manager
179
+ self.settings_manager = DatabaseCurlSettingsManager(db_settings_manager, logger=self.logger)
180
+ self.logger.info("cURL Tool using database backend for settings")
181
+ elif CURL_SETTINGS_AVAILABLE:
182
+ # Fallback to JSON-based settings manager
164
183
  self.settings_manager = CurlSettingsManager(logger=self.logger)
184
+ self.logger.info("cURL Tool using JSON backend for settings (database not available)")
165
185
  else:
166
186
  self.settings_manager = None
167
187
  self.logger.error("cURL Settings Manager not available")
@@ -174,7 +194,7 @@ class CurlToolWidget:
174
194
 
175
195
  # Settings - load from settings manager or use defaults (must be loaded before history manager)
176
196
  if self.settings_manager:
177
- self.settings = self.settings_manager.get_all_settings()
197
+ self.settings = self.settings_manager.load_settings()
178
198
  else:
179
199
  self.settings = {
180
200
  "timeout": 30,
@@ -190,16 +210,26 @@ class CurlToolWidget:
190
210
  "history_retention_days": 30
191
211
  }
192
212
 
213
+ # Initialize history manager - use database backend if available
193
214
  if CURL_HISTORY_AVAILABLE:
194
215
  max_history = self.settings.get("max_history_items", 100)
195
- # Use centralized settings.json file
196
- history_file = os.path.abspath("settings.json")
197
- self.history_manager = CurlHistoryManager(
198
- history_file=history_file,
199
- max_items=max_history,
200
- logger=self.logger
201
- )
202
- self.logger.debug(f"History manager initialized with file: {history_file}")
216
+ if db_settings_manager and DATABASE_CURL_SETTINGS_AVAILABLE:
217
+ # Use database backend for history (through DatabaseCurlSettingsManager)
218
+ self.history_manager = CurlHistoryManager(
219
+ max_items=max_history,
220
+ logger=self.logger,
221
+ db_settings_manager=db_settings_manager
222
+ )
223
+ self.logger.debug("History manager initialized with database backend")
224
+ else:
225
+ # Fallback to JSON file
226
+ history_file = os.path.abspath("settings.json")
227
+ self.history_manager = CurlHistoryManager(
228
+ history_file=history_file,
229
+ max_items=max_history,
230
+ logger=self.logger
231
+ )
232
+ self.logger.debug(f"History manager initialized with file: {history_file}")
203
233
  else:
204
234
  self.history_manager = None
205
235
  self.logger.error("cURL History Manager not available")
@@ -458,7 +458,8 @@ class DiffViewerWidget:
458
458
 
459
459
  def show(self):
460
460
  """Show the diff viewer."""
461
- self.diff_frame.grid(row=0, column=0, sticky="nsew", pady=5)
461
+ # Use row=1 (same as central_frame) to not cover search bar in row=0
462
+ self.diff_frame.grid(row=1, column=0, sticky="nsew", pady=5)
462
463
 
463
464
  def hide(self):
464
465
  """Hide the diff viewer."""
@@ -1658,14 +1659,6 @@ class DiffViewerSettingsWidget:
1658
1659
  variable=self.syntax_var,
1659
1660
  command=self._on_syntax_change
1660
1661
  ).pack(side=tk.LEFT, padx=(0, 8))
1661
-
1662
- ttk.Label(row2, text="|").pack(side=tk.LEFT, padx=5)
1663
-
1664
- ttk.Button(
1665
- row2,
1666
- text="List Comparator",
1667
- command=self._launch_list_comparator
1668
- ).pack(side=tk.LEFT, padx=5)
1669
1662
 
1670
1663
  def _on_option_change(self):
1671
1664
  """Handle option change."""
@@ -9,10 +9,20 @@ Author: Pomera AI Commander Team
9
9
 
10
10
  import importlib
11
11
  import logging
12
+ import threading
12
13
  from typing import Dict, Any, Optional, Callable, Type, List, Tuple
13
14
  from dataclasses import dataclass, field
14
15
  from enum import Enum
15
16
 
17
+ # Optional rapidfuzz for fuzzy search
18
+ try:
19
+ from rapidfuzz import fuzz, process
20
+ RAPIDFUZZ_AVAILABLE = True
21
+ except ImportError:
22
+ fuzz = None
23
+ process = None
24
+ RAPIDFUZZ_AVAILABLE = False
25
+
16
26
 
17
27
  logger = logging.getLogger(__name__)
18
28
 
@@ -68,12 +78,11 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
68
78
  description="Transform text case (uppercase, lowercase, title case, etc.)",
69
79
  available_flag="CASE_TOOL_MODULE_AVAILABLE"
70
80
  ),
71
- "Find & Replace": ToolSpec(
72
- name="Find & Replace",
81
+ "Find & Replace Text": ToolSpec(
82
+ name="Find & Replace Text",
73
83
  module_path="tools.find_replace",
74
84
  class_name="FindReplaceWidget",
75
85
  category=ToolCategory.CORE,
76
- is_widget=True,
77
86
  description="Find and replace text with regex support",
78
87
  available_flag="FIND_REPLACE_MODULE_AVAILABLE"
79
88
  ),
@@ -83,7 +92,6 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
83
92
  class_name="DiffViewerWidget",
84
93
  widget_class="DiffViewerSettingsWidget",
85
94
  category=ToolCategory.CORE,
86
- is_widget=True,
87
95
  description="Compare and view differences between texts",
88
96
  available_flag="DIFF_VIEWER_MODULE_AVAILABLE"
89
97
  ),
@@ -94,7 +102,6 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
94
102
  module_path="tools.ai_tools",
95
103
  class_name="AIToolsWidget",
96
104
  category=ToolCategory.AI,
97
- is_widget=True,
98
105
  description="AI-powered text processing with multiple providers",
99
106
  available_flag="AI_TOOLS_AVAILABLE"
100
107
  ),
@@ -158,8 +165,8 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
158
165
  ),
159
166
 
160
167
  # Conversion Tools
161
- "Base64 Tools": ToolSpec(
162
- name="Base64 Tools",
168
+ "Base64 Encoder/Decoder": ToolSpec(
169
+ name="Base64 Encoder/Decoder",
163
170
  module_path="tools.base64_tools",
164
171
  class_name="Base64Tools",
165
172
  widget_class="Base64ToolsWidget",
@@ -334,6 +341,7 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
334
341
  module_path="tools.list_comparator",
335
342
  class_name="DiffApp",
336
343
  category=ToolCategory.UTILITY,
344
+ is_widget=True, # Exclude from search - standalone widget
337
345
  description="Compare two lists and find differences",
338
346
  available_flag="LIST_COMPARATOR_MODULE_AVAILABLE"
339
347
  ),
@@ -367,6 +375,89 @@ TOOL_SPECS: Dict[str, ToolSpec] = {
367
375
  ),
368
376
  }
369
377
 
378
+ # These sub-tools appear as tabs within their parent tool
379
+ PARENT_TOOLS = {
380
+ "AI Tools": [
381
+ # Tab order from UI screenshot
382
+ "Google AI",
383
+ "Vertex AI",
384
+ "Azure AI",
385
+ "Anthropic AI",
386
+ "OpenAI",
387
+ "Cohere AI",
388
+ "HuggingFace AI",
389
+ "Groq AI",
390
+ "OpenRouterAI",
391
+ "LM Studio",
392
+ "AWS Bedrock",
393
+ ],
394
+ "Extraction Tools": [
395
+ "Email Extraction",
396
+ "HTML Tool",
397
+ "Regex Extractor",
398
+ "URL Link Extractor",
399
+ ],
400
+ "Generator Tools": [
401
+ # Tab order: Strong Password | Repeating Text | Lorem Ipsum | UUID/GUID | Random Email | ASCII Art | Hash | Slug
402
+ "Strong Password Generator",
403
+ "Repeating Text Generator",
404
+ "Lorem Ipsum Generator",
405
+ "UUID/GUID Generator",
406
+ "Random Email Generator",
407
+ "ASCII Art Generator",
408
+ "Hash Generator",
409
+ "Slug Generator",
410
+ ],
411
+ "Line Tools": [
412
+ # Tab order from line_tools.py
413
+ "Remove Duplicates",
414
+ "Remove Empty Lines",
415
+ "Add Line Numbers",
416
+ "Remove Line Numbers",
417
+ "Reverse Lines",
418
+ "Shuffle Lines",
419
+ ],
420
+ "Markdown Tools": [
421
+ # Tab order from markdown_tools.py
422
+ "Strip Markdown",
423
+ "Extract Links",
424
+ "Extract Headers",
425
+ "Table to CSV",
426
+ "Format Table",
427
+ ],
428
+ "Sorter Tools": [
429
+ # Tab order from sorter_tools.py
430
+ "Number Sorter",
431
+ "Alphabetical Sorter",
432
+ ],
433
+ "Text Wrapper": [
434
+ # Tab order from text_wrapper.py
435
+ "Word Wrap",
436
+ "Justify Text",
437
+ "Prefix/Suffix",
438
+ "Indent Text",
439
+ "Quote Text",
440
+ ],
441
+ "Translator Tools": [
442
+ # Tab order from translator_tools.py
443
+ "Morse Code Translator",
444
+ "Binary Code Translator",
445
+ ],
446
+ "Whitespace Tools": [
447
+ # Tab order from whitespace_tools.py
448
+ "Trim Lines",
449
+ "Remove Extra Spaces",
450
+ "Tabs/Spaces Converter",
451
+ "Normalize Line Endings",
452
+ ],
453
+ }
454
+
455
+ # Reverse lookup: sub-tool -> parent
456
+ SUB_TOOL_PARENTS = {
457
+ sub: parent
458
+ for parent, subs in PARENT_TOOLS.items()
459
+ for sub in subs
460
+ }
370
461
 
371
462
  class ToolLoader:
372
463
  """
@@ -588,6 +679,75 @@ class ToolLoader:
588
679
  if spec.category == category and self.is_available(name)
589
680
  ]
590
681
 
682
+ def get_processing_tools(self) -> List[str]:
683
+ """
684
+ Get list of available text processing tools (excludes standalone widgets).
685
+
686
+ This filters out tools like Notes Widget, MCP Manager that are
687
+ standalone windows rather than text processing tools.
688
+
689
+ Returns:
690
+ List of processing tool names
691
+ """
692
+ return [
693
+ name for name, spec in self._specs.items()
694
+ if self.is_available(name) and not spec.is_widget
695
+ ]
696
+
697
+ def get_processing_tools_by_category(self, category: ToolCategory) -> List[str]:
698
+ """
699
+ Get processing tools in a specific category (excludes standalone widgets).
700
+
701
+ Args:
702
+ category: Tool category
703
+
704
+ Returns:
705
+ List of processing tool names in that category
706
+ """
707
+ return [
708
+ name for name, spec in self._specs.items()
709
+ if spec.category == category and self.is_available(name) and not spec.is_widget
710
+ ]
711
+
712
+ def get_grouped_tools(self) -> List[Tuple[str, bool]]:
713
+ """
714
+ Get processing tools grouped with parent-child relationships.
715
+
716
+ Returns tools sorted alphabetically, with sub-tools appearing
717
+ immediately after their parent tool.
718
+
719
+ Returns:
720
+ List of tuples: (tool_name, is_sub_tool)
721
+ is_sub_tool is True for tools that belong under a parent
722
+ """
723
+ # Get all processing tools (excludes widgets)
724
+ all_tools = set(self.get_processing_tools())
725
+
726
+ # Build result with grouping
727
+ result: List[Tuple[str, bool]] = []
728
+ processed = set()
729
+
730
+ # Sort main tools alphabetically
731
+ main_tools = sorted([t for t in all_tools if t not in SUB_TOOL_PARENTS])
732
+
733
+ for tool in main_tools:
734
+ if tool in processed:
735
+ continue
736
+
737
+ # Add the main tool
738
+ result.append((tool, False))
739
+ processed.add(tool)
740
+
741
+ # If this is a parent, add all its children (they're virtual sub-tools for tabs)
742
+ if tool in PARENT_TOOLS:
743
+ children = PARENT_TOOLS[tool]
744
+ for child in children:
745
+ if child not in processed:
746
+ result.append((child, True))
747
+ processed.add(child)
748
+
749
+ return result
750
+
591
751
  def get_tool_spec(self, tool_name: str) -> Optional[ToolSpec]:
592
752
  """
593
753
  Get the specification for a tool.
@@ -657,6 +817,86 @@ class ToolLoader:
657
817
  self._widget_classes.clear()
658
818
  logger.debug("Tool loader cache cleared")
659
819
 
820
+ def search_tools(
821
+ self,
822
+ query: str,
823
+ limit: int = 10,
824
+ include_unavailable: bool = False
825
+ ) -> List[Tuple[str, int, 'ToolCategory']]:
826
+ """
827
+ Fuzzy search tools by name and description.
828
+
829
+ Uses rapidfuzz for fuzzy matching if available, otherwise falls back
830
+ to simple substring matching.
831
+
832
+ Args:
833
+ query: Search query string
834
+ limit: Maximum number of results to return
835
+ include_unavailable: Whether to include unavailable tools
836
+
837
+ Returns:
838
+ List of tuples: (tool_name, match_score, category)
839
+ Score is 0-100, higher is better match
840
+ """
841
+ if not query:
842
+ # Return all tools sorted alphabetically
843
+ tools = self.get_available_tools() if not include_unavailable else list(self._specs.keys())
844
+ return [(name, 100, self._specs[name].category) for name in sorted(tools)[:limit]]
845
+
846
+ # Build search data: name -> searchable text
847
+ search_data = {}
848
+ for name, spec in self._specs.items():
849
+ if not include_unavailable and not self.is_available(name):
850
+ continue
851
+ # Combine name and description for searching
852
+ search_text = f"{name} {spec.description}"
853
+ search_data[name] = search_text
854
+
855
+ if not search_data:
856
+ return []
857
+
858
+ results: List[Tuple[str, int, ToolCategory]] = []
859
+
860
+ if RAPIDFUZZ_AVAILABLE and process is not None:
861
+ # Use rapidfuzz for proper fuzzy matching
862
+ matches = process.extract(
863
+ query,
864
+ search_data,
865
+ scorer=fuzz.WRatio,
866
+ limit=limit
867
+ )
868
+
869
+ for match in matches:
870
+ tool_name = match[2] # Key from dict
871
+ score = int(match[1]) # Score 0-100
872
+ if score >= 30: # Minimum relevance threshold
873
+ category = self._specs[tool_name].category
874
+ results.append((tool_name, score, category))
875
+ else:
876
+ # Fallback to simple substring matching
877
+ query_lower = query.lower()
878
+ for name, search_text in search_data.items():
879
+ text_lower = search_text.lower()
880
+ if query_lower in text_lower:
881
+ # Score based on position and exact match
882
+ if name.lower() == query_lower:
883
+ score = 100
884
+ elif name.lower().startswith(query_lower):
885
+ score = 90
886
+ elif query_lower in name.lower():
887
+ score = 80
888
+ else:
889
+ score = 60 # Match in description only
890
+
891
+ category = self._specs[name].category
892
+ results.append((name, score, category))
893
+
894
+ # Sort by score descending, then name ascending
895
+ results.sort(key=lambda x: (-x[1], x[0]))
896
+ results = results[:limit]
897
+
898
+ return results
899
+
660
900
  def get_legacy_flags(self) -> Dict[str, bool]:
661
901
  """
662
902
  Get legacy availability flags for backwards compatibility.
@@ -671,26 +911,33 @@ class ToolLoader:
671
911
  return flags
672
912
 
673
913
 
674
- # Global instance
914
+ # Global instance with thread-safe initialization
675
915
  _tool_loader: Optional[ToolLoader] = None
916
+ _tool_loader_lock = threading.Lock()
676
917
 
677
918
 
678
919
  def get_tool_loader() -> ToolLoader:
679
920
  """
680
- Get the global tool loader instance.
921
+ Get the global tool loader instance (thread-safe).
922
+
923
+ Uses double-checked locking pattern to ensure thread safety
924
+ while minimizing lock contention after initialization.
681
925
 
682
926
  Returns:
683
927
  Global ToolLoader instance
684
928
  """
685
929
  global _tool_loader
686
930
  if _tool_loader is None:
687
- _tool_loader = ToolLoader()
931
+ with _tool_loader_lock:
932
+ # Double-check after acquiring lock
933
+ if _tool_loader is None:
934
+ _tool_loader = ToolLoader()
688
935
  return _tool_loader
689
936
 
690
937
 
691
938
  def init_tool_loader(tool_specs: Optional[Dict[str, ToolSpec]] = None) -> ToolLoader:
692
939
  """
693
- Initialize the global tool loader.
940
+ Initialize the global tool loader (thread-safe).
694
941
 
695
942
  Args:
696
943
  tool_specs: Optional custom tool specifications
@@ -699,12 +946,13 @@ def init_tool_loader(tool_specs: Optional[Dict[str, ToolSpec]] = None) -> ToolLo
699
946
  Initialized ToolLoader
700
947
  """
701
948
  global _tool_loader
702
- _tool_loader = ToolLoader(tool_specs)
949
+ with _tool_loader_lock:
950
+ _tool_loader = ToolLoader(tool_specs)
703
951
  return _tool_loader
704
952
 
705
953
 
706
954
  def reset_tool_loader() -> None:
707
- """Reset the global tool loader."""
955
+ """Reset the global tool loader (thread-safe)."""
708
956
  global _tool_loader
709
- _tool_loader = None
710
-
957
+ with _tool_loader_lock:
958
+ _tool_loader = None