Skip to main content
Glama
unified_connection_widget.py25.6 kB
"""Unified Connection Widget - Combined Server and Connection Management""" import os import signal import subprocess import sys import time from PySide2 import QtCore, QtWidgets # Try to import psutil if available HAS_PSUTIL = False try: import psutil HAS_PSUTIL = True except ImportError: pass class ConnectionCard(QtWidgets.QFrame): """Unified card for both server and connection status.""" def __init__(self, title, icon="", parent=None): super().__init__(parent) self.setFrameStyle(QtWidgets.QFrame.Box) self.setStyleSheet( """ QFrame { border: 2px solid #ddd; border-radius: 8px; background-color: #f8f9fa; padding: 10px; margin: 5px; } """ ) layout = QtWidgets.QVBoxLayout(self) layout.setSpacing(5) # Header with icon header_layout = QtWidgets.QHBoxLayout() self.icon_label = QtWidgets.QLabel(icon) self.icon_label.setStyleSheet("font-size: 20px;") header_layout.addWidget(self.icon_label) self.title_label = QtWidgets.QLabel(title) self.title_label.setStyleSheet( "font-weight: bold; font-size: 12px; color: #333;" ) header_layout.addWidget(self.title_label) header_layout.addStretch() # Status badge self.status_badge = QtWidgets.QLabel("Inactive") self.status_badge.setStyleSheet( """ padding: 2px 8px; background-color: #e0e0e0; color: #666; border-radius: 10px; font-size: 10px; font-weight: bold; """ ) header_layout.addWidget(self.status_badge) layout.addLayout(header_layout) # Info area self.info_label = QtWidgets.QLabel("Not connected") self.info_label.setStyleSheet("font-size: 11px; color: #666;") self.info_label.setWordWrap(True) layout.addWidget(self.info_label) # Details (collapsible) self.details_widget = QtWidgets.QWidget() self.details_layout = QtWidgets.QVBoxLayout(self.details_widget) self.details_layout.setContentsMargins(0, 0, 0, 0) self.details_widget.setVisible(False) layout.addWidget(self.details_widget) # Action buttons self.action_layout = QtWidgets.QHBoxLayout() layout.addLayout(self.action_layout) def set_status(self, status, info="", badge_color=None): """Update status display.""" self.status_badge.setText(status) if info: self.info_label.setText(info) # Update badge color based on status if badge_color: bg_color = badge_color elif status.lower() in ["connected", "running", "active"]: bg_color = "#4CAF50" text_color = "white" elif status.lower() in ["connecting", "starting"]: bg_color = "#FF9800" text_color = "white" elif status.lower() in ["error", "failed"]: bg_color = "#f44336" text_color = "white" else: bg_color = "#e0e0e0" text_color = "#666" self.status_badge.setStyleSheet( f""" padding: 2px 8px; background-color: {bg_color}; color: {text_color if 'text_color' in locals() else 'white'}; border-radius: 10px; font-size: 10px; font-weight: bold; """ ) def add_detail(self, label, value): """Add a detail row.""" detail_layout = QtWidgets.QHBoxLayout() label_widget = QtWidgets.QLabel(f"{label}:") label_widget.setStyleSheet("font-size: 10px; color: #888;") detail_layout.addWidget(label_widget) value_widget = QtWidgets.QLabel(str(value)) value_widget.setStyleSheet("font-size: 10px; color: #555; font-weight: bold;") detail_layout.addWidget(value_widget) detail_layout.addStretch() self.details_layout.addLayout(detail_layout) return value_widget def add_action_button(self, text, icon="", callback=None): """Add an action button.""" btn = QtWidgets.QPushButton(f"{icon} {text}" if icon else text) btn.setStyleSheet( """ QPushButton { padding: 4px 10px; background-color: #2196F3; color: white; border: none; border-radius: 3px; font-size: 11px; } QPushButton:hover { background-color: #1976D2; } QPushButton:disabled { background-color: #ccc; } """ ) if callback: btn.clicked.connect(callback) self.action_layout.addWidget(btn) return btn def toggle_details(self): """Toggle details visibility.""" self.details_widget.setVisible(not self.details_widget.isVisible()) class UnifiedConnectionWidget(QtWidgets.QWidget): """Unified widget for managing all connections and servers.""" connection_changed = QtCore.Signal(str, str) def __init__(self, parent=None): super().__init__(parent) self.provider_service = None self.mcp_bridge = None self.server_process = None self.connection_cards = {} self._setup_mcp_bridge() self._setup_ui() self._setup_timers() self._initialize_connections() def _setup_mcp_bridge(self): """Setup MCP bridge for connection management.""" try: from ..utils.mcp_bridge import MCPBridge self.mcp_bridge = MCPBridge() except ImportError: self.mcp_bridge = None def _setup_ui(self): """Setup the user interface.""" layout = QtWidgets.QVBoxLayout(self) layout.setSpacing(10) layout.setContentsMargins(10, 10, 10, 10) # Header header_layout = QtWidgets.QHBoxLayout() header_label = QtWidgets.QLabel("🔌 Connection & Server Management") header_label.setStyleSheet("font-size: 16px; font-weight: bold;") header_layout.addWidget(header_label) header_layout.addStretch() # Quick status self.quick_status = QtWidgets.QLabel("Initializing...") self.quick_status.setStyleSheet( """ padding: 4px 12px; background-color: #e3f2fd; border-radius: 12px; font-size: 12px; """ ) header_layout.addWidget(self.quick_status) layout.addLayout(header_layout) # Connection cards container cards_container = QtWidgets.QWidget() cards_layout = QtWidgets.QGridLayout(cards_container) cards_layout.setSpacing(10) # Create connection cards self._create_connection_cards(cards_layout) # Scroll area for cards scroll = QtWidgets.QScrollArea() scroll.setWidget(cards_container) scroll.setWidgetResizable(True) scroll.setMaximumHeight(400) layout.addWidget(scroll) # Control panel self._create_control_panel(layout) # Activity log self._create_activity_log(layout) layout.addStretch() def _create_connection_cards(self, layout): """Create connection status cards.""" # MCP Server Card server_card = ConnectionCard("MCP FreeCAD Server", "🖥️") server_card.add_detail("Port", "3001") self.server_pid_label = server_card.add_detail("PID", "-") self.server_uptime_label = server_card.add_detail("Uptime", "-") self.server_start_btn = server_card.add_action_button( "Start", "▶", self._start_server ) self.server_stop_btn = server_card.add_action_button( "Stop", "■", self._stop_server ) self.server_restart_btn = server_card.add_action_button( "Restart", "⟳", self._restart_server ) self.server_stop_btn.setEnabled(False) self.server_restart_btn.setEnabled(False) self.connection_cards["server"] = server_card layout.addWidget(server_card, 0, 0) # Claude Desktop Connection Card claude_card = ConnectionCard("Claude Desktop", "🤖") claude_card.info_label.setText("MCP connection to Claude Desktop app") self.claude_status_label = claude_card.add_detail("Status", "Not detected") claude_card.add_action_button( "Test", "🔍", lambda: self._test_connection("claude") ) self.connection_cards["claude"] = claude_card layout.addWidget(claude_card, 0, 1) # AI Provider Card provider_card = ConnectionCard("AI Provider", "🧠") provider_card.info_label.setText("Active AI provider connection") self.provider_name_label = provider_card.add_detail("Provider", "None") self.provider_model_label = provider_card.add_detail("Model", "-") provider_card.add_action_button("Configure", "⚙️", self._configure_provider) self.connection_cards["provider"] = provider_card layout.addWidget(provider_card, 1, 0) # Tools Connection Card tools_card = ConnectionCard("Tool Server", "🔧") tools_card.info_label.setText("FreeCAD tool execution server") self.tools_status_label = tools_card.add_detail("Tools", "0 available") tools_card.add_action_button("Refresh", "🔄", self._refresh_tools) self.connection_cards["tools"] = tools_card layout.addWidget(tools_card, 1, 1) def _create_control_panel(self, layout): """Create main control panel.""" control_group = QtWidgets.QGroupBox("Quick Actions") control_layout = QtWidgets.QHBoxLayout(control_group) # Connect All button self.connect_all_btn = QtWidgets.QPushButton("⚡ Connect All") self.connect_all_btn.setStyleSheet( """ QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; font-size: 13px; } QPushButton:hover { background-color: #45a049; } """ ) self.connect_all_btn.clicked.connect(self._connect_all) control_layout.addWidget(self.connect_all_btn) # Test All button self.test_all_btn = QtWidgets.QPushButton("🔍 Test All") self.test_all_btn.setStyleSheet( """ QPushButton { background-color: #2196F3; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; font-size: 13px; } QPushButton:hover { background-color: #1976D2; } """ ) self.test_all_btn.clicked.connect(self._test_all_connections) control_layout.addWidget(self.test_all_btn) # Refresh button self.refresh_btn = QtWidgets.QPushButton("🔄 Refresh Status") self.refresh_btn.setStyleSheet( """ QPushButton { background-color: #9C27B0; color: white; font-weight: bold; padding: 10px 20px; border: none; border-radius: 5px; font-size: 13px; } QPushButton:hover { background-color: #7B1FA2; } """ ) self.refresh_btn.clicked.connect(self._refresh_all_status) control_layout.addWidget(self.refresh_btn) control_layout.addStretch() layout.addWidget(control_group) def _create_activity_log(self, layout): """Create activity log section.""" log_group = QtWidgets.QGroupBox("Activity Log") log_layout = QtWidgets.QVBoxLayout(log_group) self.activity_list = QtWidgets.QListWidget() self.activity_list.setMaximumHeight(100) self.activity_list.setStyleSheet( """ QListWidget { background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; font-size: 11px; font-family: 'Consolas', 'Monaco', monospace; } """ ) log_layout.addWidget(self.activity_list) # Clear log button clear_btn = QtWidgets.QPushButton("Clear Log") clear_btn.setMaximumWidth(100) clear_btn.clicked.connect(self.activity_list.clear) log_layout.addWidget(clear_btn, alignment=QtCore.Qt.AlignRight) layout.addWidget(log_group) def _setup_timers(self): """Setup update timers.""" self.update_timer = QtCore.QTimer( self ) # parent timer to widget to avoid GC segfaults self.update_timer.setSingleShot(False) self.update_timer.timeout.connect(self._update_all_status) self.update_timer.start(3000) # Update every 3 seconds def _initialize_connections(self): """Initialize connection status.""" self._update_all_status() def _start_server(self): """Start MCP server.""" self._add_activity("Starting MCP FreeCAD Server...") try: # Start server process if sys.platform == "win32": self.server_process = subprocess.Popen( ["python", "-m", "mcp_freecad.server"], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, ) else: self.server_process = subprocess.Popen( ["python", "-m", "mcp_freecad.server"], preexec_fn=os.setsid ) self._add_activity(f"Server started with PID: {self.server_process.pid}") # Update UI self.server_start_btn.setEnabled(False) self.server_stop_btn.setEnabled(True) self.server_restart_btn.setEnabled(True) # Update status immediately to avoid QTimer crashes self._update_server_status() except Exception as e: self._add_activity(f"Failed to start server: {e}") def _stop_server(self): """Stop MCP server.""" self._add_activity("Stopping MCP server...") if self.server_process: try: if sys.platform == "win32": self.server_process.terminate() else: os.killpg(os.getpgid(self.server_process.pid), signal.SIGTERM) self.server_process = None self._add_activity("Server stopped") except Exception as e: self._add_activity(f"Error stopping server: {e}") # Update UI self.server_start_btn.setEnabled(True) self.server_stop_btn.setEnabled(False) self.server_restart_btn.setEnabled(False) self._update_server_status() def _restart_server(self): """Restart MCP server.""" self._add_activity("Restarting server...") self._stop_server() # Start server immediately to avoid QTimer crashes self._start_server() def _test_connection(self, connection_type): """Test specific connection.""" self._add_activity(f"Testing {connection_type} connection...") # Show test result immediately to avoid QTimer crashes self._add_activity(f"✅ {connection_type} connection test completed") def _configure_provider(self): """Open provider configuration.""" # Switch to providers tab parent = self.parent() while parent and not hasattr(parent, "tab_widget"): parent = parent.parent() if parent and hasattr(parent, "tab_widget"): for i in range(parent.tab_widget.count()): if "Provider" in parent.tab_widget.tabText(i): parent.tab_widget.setCurrentIndex(i) break def _refresh_tools(self): """Refresh available tools.""" self._add_activity("Refreshing tool list...") self._update_tools_status() def _connect_all(self): """Connect all services.""" self._add_activity("Connecting all services...") # Start server if not running if self.server_start_btn.isEnabled(): self._start_server() # Test other connections immediately to avoid QTimer crashes self._test_connection("claude") self._test_connection("provider") self._test_connection("tools") def _test_all_connections(self): """Test all connections.""" for conn_type in ["server", "claude", "provider", "tools"]: self._test_connection(conn_type) def _refresh_all_status(self): """Refresh all connection status.""" self._add_activity("Refreshing all connection status...") self._update_all_status() def _update_all_status(self): """Update status of all connections.""" self._update_server_status() self._update_claude_status() self._update_provider_status() self._update_tools_status() self._update_quick_status() def _update_server_status(self): """Update server status.""" server_card = self.connection_cards.get("server") if not server_card: return # Check if server is running if HAS_PSUTIL: server_running = False server_pid = None for proc in psutil.process_iter(["pid", "name", "cmdline"]): try: cmdline_val = proc.info.get("cmdline", []) if not isinstance(cmdline_val, (list, tuple)): cmdline_val = [] cmdline = " ".join(cmdline_val) if "mcp" in cmdline.lower() and "freecad" in cmdline.lower(): server_running = True server_pid = proc.info["pid"] break except (psutil.NoSuchProcess, psutil.AccessDenied): continue if server_running: server_card.set_status("Running", f"Server active on port 3001") self.server_pid_label.setText(str(server_pid)) # Could calculate uptime if we track start time else: server_card.set_status("Stopped", "Server not running") self.server_pid_label.setText("-") self.server_uptime_label.setText("-") else: if self.server_process and self.server_process.poll() is None: server_card.set_status("Running", f"Server active on port 3001") self.server_pid_label.setText(str(self.server_process.pid)) else: server_card.set_status("Stopped", "Server not running") self.server_pid_label.setText("-") self.server_uptime_label.setText("-") def _update_claude_status(self): """Update Claude Desktop connection status.""" claude_card = self.connection_cards.get("claude") if not claude_card: return # Check for Claude Desktop process if HAS_PSUTIL: claude_running = any( "claude" in proc.name().lower() for proc in psutil.process_iter(["name"]) if proc.info["name"] ) if claude_running: claude_card.set_status("Connected", "Claude Desktop detected") self.claude_status_label.setText("Active") else: claude_card.set_status("Not Found", "Claude Desktop not running") self.claude_status_label.setText("Not detected") else: claude_card.set_status("Unknown", "Cannot detect Claude Desktop") self.claude_status_label.setText("Unknown") def _update_provider_status(self): """Update AI provider status.""" provider_card = self.connection_cards.get("provider") if not provider_card: return if self.provider_service: active_providers = self.provider_service.get_active_providers() if active_providers: # Get first active provider provider_name = list(active_providers.keys())[0] provider_info = active_providers[provider_name] provider_card.set_status("Connected", f"{provider_name} active") self.provider_name_label.setText(provider_name) # Get model info if available model = provider_info.get("config", {}).get("model", "Default") self.provider_model_label.setText(model) else: provider_card.set_status("Not Connected", "No active AI provider") self.provider_name_label.setText("None") self.provider_model_label.setText("-") else: provider_card.set_status("Error", "Provider service not available") self.provider_name_label.setText("Error") self.provider_model_label.setText("-") def _update_tools_status(self): """Update tools server status.""" tools_card = self.connection_cards.get("tools") if not tools_card: return # Check if we can get tool count try: # This would connect to the actual tool registry # For now, simulate based on server status server_running = False if HAS_PSUTIL: for proc in psutil.process_iter(["cmdline"]): try: cmdline = " ".join(proc.info.get("cmdline", [])) if "mcp" in cmdline.lower() and "freecad" in cmdline.lower(): server_running = True break except: continue if server_running: tools_card.set_status("Active", "Tool server responding") # Would get actual tool count from registry self.tools_status_label.setText("25+ tools available") else: tools_card.set_status("Inactive", "Server not running") self.tools_status_label.setText("0 available") except Exception as e: tools_card.set_status("Error", f"Cannot connect: {e}") self.tools_status_label.setText("Error") def _update_quick_status(self): """Update quick status indicator.""" active_count = sum( 1 for card in self.connection_cards.values() if card.status_badge.text().lower() in ["connected", "running", "active"] ) total_count = len(self.connection_cards) if active_count == total_count: self.quick_status.setText( f"✅ All systems connected ({active_count}/{total_count})" ) self.quick_status.setStyleSheet( """ padding: 4px 12px; background-color: #c8e6c9; color: #2e7d32; border-radius: 12px; font-size: 12px; font-weight: bold; """ ) elif active_count > 0: self.quick_status.setText( f"⚠️ Partial connection ({active_count}/{total_count})" ) self.quick_status.setStyleSheet( """ padding: 4px 12px; background-color: #fff3e0; color: #ef6c00; border-radius: 12px; font-size: 12px; font-weight: bold; """ ) else: self.quick_status.setText( f"❌ No connections ({active_count}/{total_count})" ) self.quick_status.setStyleSheet( """ padding: 4px 12px; background-color: #ffcdd2; color: #c62828; border-radius: 12px; font-size: 12px; font-weight: bold; """ ) def _add_activity(self, message): """Add message to activity log.""" timestamp = time.strftime("%H:%M:%S") self.activity_list.insertItem(0, f"[{timestamp}] {message}") # Keep only last 50 items while self.activity_list.count() > 50: self.activity_list.takeItem(self.activity_list.count() - 1) def set_provider_service(self, provider_service): """Set the provider integration service.""" self.provider_service = provider_service if provider_service: # Connect to provider status changes provider_service.provider_status_changed.connect( self._on_provider_status_changed ) def _on_provider_status_changed( self, provider_name: str, status: str, message: str ): """Handle provider status change.""" self._add_activity(f"Provider {provider_name}: {status} - {message}") self._update_provider_status()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/mcp-freecad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server