Skip to main content
Glama

MCP Interactive Service

ui_pyqt.py24.5 kB
#!/usr/bin/env python # -*- coding: utf-8 -*- """ PyQt Interface Implementation """ import asyncio import sys import logging import traceback from typing import List, Dict, Any, Optional, Union from fastmcp import Context # Import language manager try: from lang_manager import get_text except ImportError: # Fallback if lang_manager is not available def get_text(key): return key # Import PyQt5 modules (if available) try: from PyQt5.QtWidgets import ( QApplication, QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QListWidgetItem, QCheckBox, QLineEdit, QTextEdit, QPlainTextEdit, QGroupBox, QRadioButton, QButtonGroup, QScrollArea, QWidget, QDesktopWidget ) from PyQt5.QtCore import QObject, pyqtSignal, Qt PYQT_AVAILABLE = True except ImportError: PYQT_AVAILABLE = False # Configure logging logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()]) logger = logging.getLogger('PyQtUI') # Define Context type alias, making it optional ContextType = Optional[Context] # If PyQt5 is not installed, provide a placeholder class if not PYQT_AVAILABLE: class PyQtUIMissingDeps: """PyQt Interface Implementation Class (PyQt5 not installed)""" def __init__(self): """Initialize PyQt interface""" print(f"Warning: {get_text('pyqt_not_installed')}") self._pyqt_available = False async def select_option(self, options, prompt="Please select one of the following options", ctx=None): """Placeholder method""" print(get_text("pyqt_not_installed")) return { "selected_index": -1, "selected_option": None, "custom_input": get_text("pyqt_interface_unavailable"), "is_custom": True } async def request_additional_info(self, prompt, ctx=None): """Placeholder method""" print(get_text("pyqt_not_installed")) return get_text("pyqt_interface_unavailable") # Assign placeholder class to PyQtUI PyQtUI = PyQtUIMissingDeps # type: ignore else: # If PyQt5 is installed, provide full implementation class ResultEmitter(QObject): """Used to emit signals in PyQt threads""" result_ready = pyqtSignal(object) class PyQtUI: """PyQt Interface Implementation Class""" def __init__(self): """Initialize PyQt interface""" self._app = None self._pyqt_available = True self._emitter = ResultEmitter() def _ensure_app(self): """Ensure QApplication is created""" if QApplication.instance() is None: self._app = QApplication(sys.argv) else: self._app = QApplication.instance() async def select_option( self, options: List[Union[str, Dict[str, Any]]], prompt: str = "Please select one of the following options", ctx: Context = None ) -> Dict[str, Any]: """ Present options to the user and get selection using PyQt interface Args: options: List of options prompt: Prompt message ctx: FastMCP context Returns: Selection result dictionary """ if not self._pyqt_available: print(get_text("pyqt_not_installed")) return { "selected_index": -1, "selected_option": None, "custom_input": get_text("pyqt_interface_unavailable"), "is_custom": True } logger.debug(f"select_option called: options count={len(options)}") # Create event object for synchronization event = asyncio.Event() result_container = [None] # Use list to store result for modification in callback # Define callback function, called when QT window closes def on_selection_completed(selection_result): result_container[0] = selection_result event.set() # Run QT window in separate thread def run_qt_dialog(): try: app = None if not QApplication.instance(): app = QApplication([]) class OptionDialog(QDialog): def __init__(self, options, prompt): super().__init__() # Get available screen size (excludes taskbar, etc.) desktop = QDesktopWidget() available_rect = desktop.availableGeometry() # Use available area instead of full screen window_height = max(600, int(available_rect.height() * 0.7)) # Use 70% of available height, max 600px window_width = max(800, int(available_rect.width() * 0.6)) # Max 800px or 60% of available width # Set window properties with fixed size self.setWindowTitle(get_text("select_dialog_title")) self.setFixedSize(window_width, window_height) # Use fixed size for consistency # Center the window on screen self.move( available_rect.x() + (available_rect.width() - window_width) // 2, available_rect.y() + (available_rect.height() - window_height) // 2 ) # Options list self.options = options # Create main layout main_layout = QVBoxLayout() # Create scrollable content area scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Create content widget for scrollable area content_widget = QWidget() content_layout = QVBoxLayout() # Add prompt label prompt_label = QLabel(prompt) prompt_label.setWordWrap(True) prompt_label.setStyleSheet("font-weight: bold; font-size: 12pt; margin: 10px;") content_layout.addWidget(prompt_label) # Options group self.option_group = QButtonGroup(self) self.option_group.setExclusive(True) # Radio selection option_container = QGroupBox(get_text("available_options")) option_layout = QVBoxLayout() # Add predefined options for i, opt in enumerate(options): if isinstance(opt, dict): title = opt.get('title', f"Option {i+1}") desc = opt.get('description', '') text = title if desc: text += f" - {desc}" else: text = str(opt) option_button = QRadioButton(text) # Note: QRadioButton doesn't have setWordWrap, but text will wrap naturally in layout self.option_group.addButton(option_button, i) option_layout.addWidget(option_button) option_container.setLayout(option_layout) content_layout.addWidget(option_container) # Custom input area (always allowed) custom_group = QGroupBox(get_text("custom_input_group")) custom_layout = QVBoxLayout() self.custom_radio = QRadioButton(get_text("custom_answer")) self.option_group.addButton(self.custom_radio, -1) custom_layout.addWidget(self.custom_radio) # Changed from QLineEdit to QPlainTextEdit for multi-line input self.custom_input = QPlainTextEdit() self.custom_input.setPlaceholderText(get_text("custom_input_placeholder")) self.custom_input.setEnabled(False) # Initially disabled self.custom_input.setMinimumHeight(100) # Set minimum height for multi-line input self.custom_input.setMaximumHeight(150) # Limit height to prevent excessive growth # Connect custom radio button event self.custom_radio.toggled.connect(self.toggle_custom_input) custom_layout.addWidget(self.custom_input) custom_group.setLayout(custom_layout) content_layout.addWidget(custom_group) # Set content widget layout and add to scroll area content_widget.setLayout(content_layout) scroll_area.setWidget(content_widget) # Add scroll area to main layout main_layout.addWidget(scroll_area) # Button area - fixed at bottom button_layout = QHBoxLayout() submit_button = QPushButton(get_text("submit_button")) submit_button.clicked.connect(self.accept) submit_button.setMinimumHeight(35) # Ensure button is easily clickable button_layout.addStretch() button_layout.addWidget(submit_button) main_layout.addLayout(button_layout) # Set main layout self.setLayout(main_layout) def toggle_custom_input(self, enabled): """Enable/disable custom input field""" self.custom_input.setEnabled(enabled) if enabled: self.custom_input.setFocus() def get_selection(self): """Get user selection""" selected_id = self.option_group.checkedId() if selected_id == -1 and self.custom_radio.isChecked(): # User chose custom input - get text from QPlainTextEdit return { "selected_index": -1, "selected_option": None, "custom_input": self.custom_input.toPlainText(), "is_custom": True } elif selected_id >= 0: # User chose predefined option return { "selected_index": selected_id, "selected_option": self.options[selected_id], "custom_input": "", "is_custom": False } else: # User didn't select any option return { "selected_index": -1, "selected_option": None, "custom_input": get_text("no_option_selected"), "is_custom": True } # Create dialog dialog = OptionDialog(options, prompt) # Show dialog and get result if dialog.exec_(): selection_result = dialog.get_selection() on_selection_completed(selection_result) else: # User cancelled dialog on_selection_completed({ "selected_index": -1, "selected_option": None, "custom_input": get_text("user_cancelled_selection"), "is_custom": True }) # Clean up resources if app: app.quit() except Exception as e: logger.error(f"{get_text('pyqt_dialog_error')}: {e}") logger.error(traceback.format_exc()) on_selection_completed({ "selected_index": -1, "selected_option": None, "custom_input": f"{get_text('pyqt_dialog_error')}: {str(e)}", "is_custom": True }) # Execute QT dialog in thread pool try: # Notify context if ctx: await ctx.info(get_text("wait_user_select")) # Start thread loop = asyncio.get_running_loop() await loop.run_in_executor(None, run_qt_dialog) # Wait for event await event.wait() # Get result result = result_container[0] # Notify context if ctx: if result["is_custom"]: await ctx.info(f"{get_text('user_provided_info')}: {result['custom_input']}") else: await ctx.info(f"{get_text('user_selected')} {result['selected_index'] + 1}") return result except Exception as e: logger.error(f"{get_text('pyqt_ui_error')}: {e}") if ctx: await ctx.error(f"{get_text('pyqt_ui_error')}: {e}") return { "selected_index": -1, "selected_option": None, "custom_input": f"{get_text('pyqt_interface_error')}: {str(e)}", "is_custom": True } async def request_additional_info( self, prompt: str, ctx: Context = None ) -> str: """ Request supplementary information from the user using PyQt interface Args: prompt: Prompt message current_info: Current information ctx: FastMCP context Returns: User input information """ if not self._pyqt_available: print(get_text("pyqt_not_installed")) return get_text("pyqt_interface_unavailable") logger.debug(f"request_additional_info called: prompt={prompt}, current_info={current_info}") # Create event object for synchronization event = asyncio.Event() result = [""] # Use list to store result for modification in callback # Define callback function, called when QT window closes def on_input_completed(text): result[0] = text event.set() # Run QT window in separate thread def run_qt_dialog(): try: app = None if not QApplication.instance(): app = QApplication([]) class InputDialog(QDialog): def __init__(self, prompt, current_info=""): super().__init__() # Get available screen size (excludes taskbar, etc.) desktop = QDesktopWidget() available_rect = desktop.availableGeometry() # Use available area instead of full screen window_height = max(600, int(available_rect.height() * 0.7)) # Use 70% of available height, max 600px window_width = max(800, int(available_rect.width() * 0.6)) # Max 800px or 60% of available width # Set window properties with fixed size self.setWindowTitle(get_text("info_request_title")) self.setFixedSize(window_width, window_height) # Use fixed size for consistency # Center the window on screen self.move( available_rect.x() + (available_rect.width() - window_width) // 2, available_rect.y() + (available_rect.height() - window_height) // 2 ) # Create main layout main_layout = QVBoxLayout() main_layout.setSpacing(10) # Set spacing between groups main_layout.setContentsMargins(15, 15, 15, 15) # Set margins # Calculate available height for each group (minus button area and spacing) button_area_height = 50 # Button area height total_spacing = 3 * 10 + 30 # Total spacing (3 spacings + margins) available_height = window_height - button_area_height - total_spacing group_height = available_height // 2 # Divide into two equal groups # First group: Prompt information prompt_label = QLabel(get_text("prompt_label") + ":") prompt_label.setStyleSheet("font-weight: bold; font-size: 10pt;") prompt_label.setFixedHeight(25) # Fixed height for small label main_layout.addWidget(prompt_label) self.prompt_edit = QPlainTextEdit(prompt) self.prompt_edit.setReadOnly(True) self.prompt_edit.setFixedHeight(group_height - 25) # Minus label height self.prompt_edit.setStyleSheet("background-color: #f5f5f5; border: 1px solid #ccc;") main_layout.addWidget(self.prompt_edit) # Second group: User input input_label = QLabel(get_text("user_input_label") + ":") input_label.setStyleSheet("font-weight: bold; font-size: 10pt; color: #2c5aa0;") input_label.setFixedHeight(25) # Fixed height for small label main_layout.addWidget(input_label) self.input_field = QPlainTextEdit() self.input_field.setPlaceholderText(get_text("input_placeholder")) self.input_field.setFixedHeight(group_height - 25) # Minus label height self.input_field.setStyleSheet("border: 2px solid #2c5aa0; border-radius: 4px;") self.input_field.setFocus() # Default focus on input field main_layout.addWidget(self.input_field) # Add buttons - fixed at bottom button_layout = QHBoxLayout() submit_button = QPushButton(get_text("submit_button")) submit_button.clicked.connect(self.accept) submit_button.setMinimumHeight(35) # Ensure button is easily clickable submit_button.setStyleSheet("QPushButton { background-color: #2c5aa0; color: white; font-weight: bold; border-radius: 4px; } QPushButton:hover { background-color: #1e3d6f; }") button_layout.addStretch() button_layout.addWidget(submit_button) main_layout.addLayout(button_layout) # Apply main layout self.setLayout(main_layout) def get_input(self): """Get user input from the text field""" return self.input_field.toPlainText() dialog = InputDialog(prompt, current_info) if dialog.exec_(): user_input = dialog.get_input() on_input_completed(user_input) else: # User cancelled, return empty string on_input_completed("") if app: app.quit() except Exception as e: logger.error(f"{get_text('pyqt_dialog_error')}: {e}") logger.error(traceback.format_exc()) on_input_completed(f"{get_text('pyqt_dialog_error')}: {str(e)}") # Execute QT dialog in thread pool try: # Notify context if ctx: await ctx.info(get_text("wait_user_input")) # Start thread loop = asyncio.get_running_loop() await loop.run_in_executor(None, run_qt_dialog) # Wait for event await event.wait() # Return result if ctx: await ctx.info(get_text("user_provided_info")) return result[0] except Exception as e: logger.error(f"{get_text('pyqt_ui_error')}: {e}") if ctx: await ctx.error(f"{get_text('pyqt_ui_error')}: {e}") return f"{get_text('pyqt_interface_error')}: {str(e)}" def cleanup(self): """Clean up resources""" # PyQt application will automatically clean up when program exits pass

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/DanielZhao1990/interaction-mcp'

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