Skip to main content
Glama
config.py20.2 kB
#!/usr/bin/env python # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai __license__ = 'GPL v3' __copyright__ = '2025, Miguel Iglesias' __docformat__ = 'restructuredtext en' from calibre.utils.config import JSONConfig from calibre.utils.localization import _ from qt.core import ( QWidget, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QVBoxLayout, QGroupBox, QComboBox, QCheckBox, ) import os from .providers import ( ensure_model_prefs, get_selected_model, list_enabled_providers, set_selected_model, describe_provider, ) # This is where all preferences for this plugin will be stored. # Name is global, so keep it reasonably unique. prefs = JSONConfig('plugins/mcp_server_recherche') # New settings for MCP server / AI prefs.defaults['server_host'] = '127.0.0.1' prefs.defaults['server_port'] = '8765' prefs.defaults['library_path'] = '' # Use current calibre library when empty prefs.defaults['api_key'] = '' # Optional AI key (e.g. OpenAI) prefs.defaults['models'] = {} prefs.defaults['selected_model'] = {} prefs.defaults['use_active_library'] = True prefs.defaults['python_executable'] = '' prefs.defaults['auto_detect_python'] = True # Defaults fuer Recherche-Agent prefs.defaults['max_query_variants'] = 3 prefs.defaults['max_hits_per_query'] = 6 prefs.defaults['max_hits_total'] = 12 prefs.defaults['target_sources'] = 3 prefs.defaults['max_excerpts'] = 4 prefs.defaults['max_excerpt_chars'] = 1200 prefs.defaults['context_hit_limit'] = 8 prefs.defaults['request_timeout'] = 15 prefs.defaults['min_hits_required'] = 3 prefs.defaults['max_refinement_rounds'] = 2 prefs.defaults['context_influence'] = 50 prefs.defaults['max_search_rounds'] = 2 # Benutzeranpassbare Prompt-Zusaetze prefs.defaults['query_planner_hint'] = '' prefs.defaults['answer_style_hint'] = '' # Hint fuer Schlagwort-Extraktion prefs.defaults['keyword_extraction_hint'] = '' # Optionaler zusaetzlicher LLM-Schlagwort-Lauf in anderer Sprache prefs.defaults['second_keyword_language_enabled'] = False prefs.defaults['second_keyword_language'] = 'Englisch' # UI-Layout-Defaults fuer den Chat-Dialog prefs.defaults['window_width'] = 800 prefs.defaults['window_height'] = 600 prefs.defaults['debug_trace_enabled'] = True ensure_model_prefs(prefs) class MCPServerRechercheConfigWidget(QWidget): """Preference widget for MCP Server Recherche plugin.""" def __init__(self): QWidget.__init__(self) layout = QVBoxLayout(self) # Connection settings ------------------------------------------------ server_group = QGroupBox(_('MCP Server Einstellungen'), self) server_form = QFormLayout(server_group) server_group.setLayout(server_form) layout.addWidget(server_group) # Server host self.host_edit = QLineEdit(self) self.host_edit.setText(prefs['server_host']) server_form.addRow(_('Server-Host:'), self.host_edit) # Server port self.port_edit = QLineEdit(self) self.port_edit.setText(prefs['server_port']) server_form.addRow(_('Server-Port:'), self.port_edit) # Library path + browse button lib_row = QHBoxLayout() self.library_edit = QLineEdit(self) self.library_edit.setText(prefs['library_path']) self.library_edit.setPlaceholderText(_('z. B. X:/E-Books')) self.use_active_checkbox = QCheckBox(_('Aktive Calibre-Bibliothek verwenden'), self) self.use_active_checkbox.setChecked(prefs['use_active_library']) self.use_active_checkbox.stateChanged.connect(self._library_mode_changed) server_form.addRow('', self.use_active_checkbox) browse_btn = QPushButton(_('Auswahl'), self) browse_btn.clicked.connect(self.choose_library) self.browse_btn = browse_btn lib_row.addWidget(self.library_edit) lib_row.addWidget(browse_btn) server_form.addRow(_('Calibre-Bibliothek:'), lib_row) python_row = QHBoxLayout() self.python_edit = QLineEdit(self) self.python_edit.setText(prefs['python_executable']) self.python_edit.setPlaceholderText(_('z. B. C:/Python/python.exe')) python_browse = QPushButton(_('Auswahl'), self) python_browse.clicked.connect(self.choose_python) python_row.addWidget(self.python_edit) python_row.addWidget(python_browse) server_form.addRow(_('Python-Interpreter (optional):'), python_row) self.python_browse = python_browse # Auto-detect Checkbox self.auto_python_checkbox = QCheckBox(_('Python automatisch ermitteln'), self) self.auto_python_checkbox.setChecked(prefs.get('auto_detect_python', True)) self.auto_python_checkbox.stateChanged.connect(self._python_mode_changed) server_form.addRow('', self.auto_python_checkbox) # Recherche-Parameter ---------------------------------------------- tuning_group = QGroupBox(_('Recherche-Feintuning'), self) tuning_form = QFormLayout(tuning_group) tuning_group.setLayout(tuning_form) layout.addWidget(tuning_group) def _make_int_edit(pref_key: str, default: int) -> QLineEdit: edit = QLineEdit(self) edit.setText(str(prefs.get(pref_key, default))) return edit self.max_query_variants_edit = _make_int_edit('max_query_variants', 3) tuning_form.addRow(_('Max. Suchvarianten:'), self.max_query_variants_edit) self.max_hits_per_query_edit = _make_int_edit('max_hits_per_query', 6) tuning_form.addRow(_('Treffer pro Query (Limit):'), self.max_hits_per_query_edit) self.max_hits_total_edit = _make_int_edit('max_hits_total', 12) tuning_form.addRow(_('Max. Treffer gesamt:'), self.max_hits_total_edit) self.target_sources_edit = _make_int_edit('target_sources', 3) tuning_form.addRow(_('Anzahl Zielquellen (frueh abbrechen bei genug Treffern):'), self.target_sources_edit) self.max_excerpts_edit = _make_int_edit('max_excerpts', 4) tuning_form.addRow(_('Max. Excerpts:'), self.max_excerpts_edit) self.max_excerpt_chars_edit = _make_int_edit('max_excerpt_chars', 1200) tuning_form.addRow(_('Excerpt-Laenge (Zeichen):'), self.max_excerpt_chars_edit) self.context_hit_limit_edit = _make_int_edit('context_hit_limit', 8) tuning_form.addRow(_('Max. Treffer im Kontextblock:'), self.context_hit_limit_edit) self.request_timeout_edit = _make_int_edit('request_timeout', 15) tuning_form.addRow(_('Timeout fuer MCP-Anfragen (Sekunden, aktuell nur informativ):'), self.request_timeout_edit) self.min_hits_required_edit = _make_int_edit('min_hits_required', 3) tuning_form.addRow(_('Mindestanzahl Treffer vor Abbruch/Refinement:'), self.min_hits_required_edit) self.max_refinement_rounds_edit = _make_int_edit('max_refinement_rounds', 2) tuning_form.addRow(_('Max. Refinement-Runden (LLM-Umformulierung):'), self.max_refinement_rounds_edit) self.max_search_rounds_edit = _make_int_edit('max_search_rounds', 2) tuning_form.addRow(_('Max. Suchrunden (Volltext + Refinements):'), self.max_search_rounds_edit) self.context_influence_edit = _make_int_edit('context_influence', 50) tuning_form.addRow(_('Kontext-Einfluss (0-100, hoeher = staerkerer Bezug auf vorige Fragen):'), self.context_influence_edit) # Benutzeranpassbare Prompt-Zusaetze self.query_planner_hint_edit = QLineEdit(self) self.query_planner_hint_edit.setText(prefs.get('query_planner_hint', '')) self.query_planner_hint_edit.setPlaceholderText( _('Optionaler Zusatz fuer die Query-Planung, z. B. "nutze immer Fachbegriffe aus der Elektrotechnik"') ) tuning_form.addRow(_('Hinweis fuer Query-Planer-Prompt:'), self.query_planner_hint_edit) self.answer_style_hint_edit = QLineEdit(self) self.answer_style_hint_edit.setText(prefs.get('answer_style_hint', '')) self.answer_style_hint_edit.setPlaceholderText( _('Optionaler Zusatz fuer die Antwort, z. B. "erklaere verstaendlich fuer Studierende"') ) tuning_form.addRow(_('Hinweis fuer Antwort-Prompt:'), self.answer_style_hint_edit) self.keyword_extraction_hint_edit = QLineEdit(self) self.keyword_extraction_hint_edit.setText(prefs.get('keyword_extraction_hint', '')) self.keyword_extraction_hint_edit.setPlaceholderText( _('Optionaler Zusatz fuer die Schlagwort-Extraktion, z. B. "nur deutsche Fachbegriffe verwenden"') ) tuning_form.addRow(_('Hinweis fuer Schlagwort-Prompt:'), self.keyword_extraction_hint_edit) # Zusatz: zweite Schlagwortsprache self.second_lang_enabled_checkbox = QCheckBox( _('Zweiten Schlagwort-Lauf in anderer Sprache verwenden'), self ) self.second_lang_enabled_checkbox.setChecked( prefs.get('second_keyword_language_enabled', False) ) tuning_form.addRow('', self.second_lang_enabled_checkbox) self.second_lang_edit = QLineEdit(self) self.second_lang_edit.setText(prefs.get('second_keyword_language', 'Englisch')) self.second_lang_edit.setPlaceholderText( _('Sprache fuer zweiten Lauf, z. B. Englisch, French, Spanish') ) tuning_form.addRow(_('Zweite Schlagwort-Sprache:'), self.second_lang_edit) # Info label info = QLabel( _( 'Host/Port konfigurieren spaeter den MCP WebSocket-Server.\n' 'Der Bibliothekspfad ueberschreibt optional die aktuelle Calibre-Bibliothek.' ), self, ) server_form.addRow(info) # AI provider settings ---------------------------------------------- provider_group = QGroupBox(_('AI Provider'), self) provider_layout = QVBoxLayout(provider_group) layout.addWidget(provider_group) self.provider_combo = QComboBox(self) self.provider_combo.currentIndexChanged.connect(self._provider_changed) provider_layout.addWidget(self.provider_combo) form = QFormLayout() provider_layout.addLayout(form) self.provider_enabled = QCheckBox(_('Provider aktiviert'), self) form.addRow('', self.provider_enabled) self.display_name_edit = QLineEdit(self) form.addRow(_('Anzeige-Name:'), self.display_name_edit) self.base_url_edit = QLineEdit(self) form.addRow(_('Base URL:'), self.base_url_edit) self.endpoint_edit = QLineEdit(self) form.addRow(_('Chat Endpoint:'), self.endpoint_edit) self.model_edit = QLineEdit(self) form.addRow(_('Standardmodell:'), self.model_edit) self.api_key_edit = QLineEdit(self) self.api_key_edit.setEchoMode(QLineEdit.Password) form.addRow(_('API Key:'), self.api_key_edit) self.temperature_edit = QLineEdit(self) form.addRow(_('Temperatur:'), self.temperature_edit) selection_row = QHBoxLayout() self.selected_provider_label = QLabel(_('Kein Provider'), self) self.selected_model_label = QLabel('', self) selection_row.addWidget(QLabel(_('Aktiv:'), self)) selection_row.addWidget(self.selected_provider_label) selection_row.addSpacing(8) selection_row.addWidget(self.selected_model_label) choose_btn = QPushButton(_('Standard setzen'), self) choose_btn.clicked.connect(self.choose_model) selection_row.addWidget(choose_btn) provider_layout.addLayout(selection_row) self._load_providers() self._update_selection_labels() self._update_library_inputs() self._update_python_inputs() # Suchmodus --------------------------------------------------------- search_group = QGroupBox(_('Suchmodus'), self) search_form = QFormLayout(search_group) search_group.setLayout(search_form) layout.addWidget(search_group) self.use_llm_planning_checkbox = QCheckBox(_('LLM fuer Query-Planung verwenden'), self) self.use_llm_planning_checkbox.setChecked(prefs.get('use_llm_query_planning', True)) search_form.addRow('', self.use_llm_planning_checkbox) self.max_keywords_edit = QLineEdit(self) self.max_keywords_edit.setText(str(prefs.get('max_search_keywords', 5))) search_form.addRow(_('Max. Schlagwoerter pro Suche:'), self.max_keywords_edit) self.bool_operator_edit = QLineEdit(self) self.bool_operator_edit.setText(prefs.get('keyword_boolean_operator', 'AND')) self.bool_operator_edit.setPlaceholderText(_('AND oder OR')) search_form.addRow(_('Verknuepfung (AND/OR):'), self.bool_operator_edit) def choose_library(self): """Select calibre library root directory.""" path = QFileDialog.getExistingDirectory( self, _('Calibre-Bibliothek auswaehlen'), self.library_edit.text() or '', ) if path: self.library_edit.setText(path) def choose_python(self): file_path, selected_filter = QFileDialog.getOpenFileName( self, _('Python-Interpreter waehlen'), self.python_edit.text() or '', _('Python (*.exe *.bat *.cmd);;Alle Dateien (*.*)') ) if file_path: self.python_edit.setText(file_path) def _library_mode_changed(self, state): prefs['use_active_library'] = bool(state) self._update_library_inputs() def _update_library_inputs(self): use_active = self.use_active_checkbox.isChecked() self.library_edit.setEnabled(not use_active) self.browse_btn.setEnabled(not use_active) def _python_mode_changed(self, state): # Persist auto-detect flag and update inputs prefs['auto_detect_python'] = bool(state) self._update_python_inputs() def _update_python_inputs(self): auto = getattr(self, 'auto_python_checkbox', None) auto_enabled = auto.isChecked() if auto is not None else True self.python_edit.setEnabled(not auto_enabled) self.python_browse.setEnabled(not auto_enabled) def save_settings(self): """Persist user changes to JSONConfig.""" prefs['server_host'] = self.host_edit.text().strip() or '127.0.0.1' prefs['server_port'] = self.port_edit.text().strip() or '8765' library_path = self.library_edit.text().strip() if library_path: library_path = os.path.normpath(library_path) prefs['library_path'] = library_path prefs['use_active_library'] = self.use_active_checkbox.isChecked() prefs['auto_detect_python'] = self.auto_python_checkbox.isChecked() prefs['python_executable'] = self.python_edit.text().strip() # Recherche-Parameter aus UI lesen (mit einfachen Fallbacks) def _read_int(edit: QLineEdit, default: int) -> int: try: value = int(edit.text().strip()) if value <= 0: raise ValueError return value except Exception: return default prefs['max_query_variants'] = _read_int(self.max_query_variants_edit, 3) prefs['max_hits_per_query'] = _read_int(self.max_hits_per_query_edit, 6) prefs['max_hits_total'] = _read_int(self.max_hits_total_edit, 12) prefs['target_sources'] = _read_int(self.target_sources_edit, 3) prefs['max_excerpts'] = _read_int(self.max_excerpts_edit, 4) prefs['max_excerpt_chars'] = _read_int(self.max_excerpt_chars_edit, 1200) prefs['context_hit_limit'] = _read_int(self.context_hit_limit_edit, 8) prefs['request_timeout'] = _read_int(self.request_timeout_edit, 15) prefs['min_hits_required'] = _read_int(self.min_hits_required_edit, 3) prefs['max_refinement_rounds'] = _read_int(self.max_refinement_rounds_edit, 2) prefs['max_search_rounds'] = _read_int(self.max_search_rounds_edit, 2) ci = _read_int(self.context_influence_edit, 50) prefs['context_influence'] = max(0, min(ci, 100)) prefs['query_planner_hint'] = self.query_planner_hint_edit.text().strip() prefs['answer_style_hint'] = self.answer_style_hint_edit.text().strip() prefs['keyword_extraction_hint'] = self.keyword_extraction_hint_edit.text().strip() prefs['second_keyword_language_enabled'] = self.second_lang_enabled_checkbox.isChecked() prefs['second_keyword_language'] = self.second_lang_edit.text().strip() or 'Englisch' # Suchmodus prefs['use_llm_query_planning'] = self.use_llm_planning_checkbox.isChecked() prefs['max_search_keywords'] = _read_int(self.max_keywords_edit, 5) op = (self.bool_operator_edit.text().strip() or 'AND').upper() if op not in ('AND', 'OR'): op = 'AND' prefs['keyword_boolean_operator'] = op self._persist_provider_settings() self._update_selection_labels() # ------------------------------------------------------------------ AI helpers def _load_providers(self): self._models = ensure_model_prefs(prefs) self.provider_combo.blockSignals(True) self.provider_combo.clear() for key, cfg in self._models.items(): self.provider_combo.addItem(describe_provider(cfg), key) self.provider_combo.blockSignals(False) self._provider_changed(self.provider_combo.currentIndex()) def _provider_changed(self, index: int): provider_key = self.provider_combo.itemData(index) cfg = self._models.get(provider_key, {}) self.provider_enabled.setChecked(bool(cfg.get('enabled'))) self.display_name_edit.setText(cfg.get('display_name', '')) self.base_url_edit.setText(cfg.get('base_url', '')) self.endpoint_edit.setText(cfg.get('chat_endpoint', '')) self.model_edit.setText(cfg.get('model', '')) self.api_key_edit.setText(cfg.get('api_key', '')) self.temperature_edit.setText(str(cfg.get('temperature', ''))) def _persist_provider_settings(self): index = self.provider_combo.currentIndex() provider_key = self.provider_combo.itemData(index) if not provider_key: return cfg = self._models.setdefault(provider_key, {}) cfg['enabled'] = self.provider_enabled.isChecked() cfg['display_name'] = self.display_name_edit.text().strip() cfg['base_url'] = self.base_url_edit.text().strip() cfg['chat_endpoint'] = self.endpoint_edit.text().strip() cfg['model'] = self.model_edit.text().strip() cfg['api_key'] = self.api_key_edit.text().strip() try: cfg['temperature'] = float(self.temperature_edit.text().strip()) except ValueError: cfg['temperature'] = 0.4 prefs['models'] = self._models def choose_model(self): models = ensure_model_prefs(prefs) enabled = list_enabled_providers(models) if not enabled: self.selected_provider_label.setText(_('Kein aktiver Provider')) self.selected_model_label.setText('') return key = self.provider_combo.itemData(self.provider_combo.currentIndex()) if key not in enabled: key = next(iter(enabled.keys())) model_name = enabled[key].get('model', '') set_selected_model(prefs, key, model_name) self._update_selection_labels() def _update_selection_labels(self): selected = get_selected_model(prefs) models = ensure_model_prefs(prefs) cfg = models.get(selected.get('provider')) if not cfg: self.selected_provider_label.setText(_('Kein Provider')) self.selected_model_label.setText('') return self.selected_provider_label.setText(describe_provider(cfg)) self.selected_model_label.setText(selected.get('model') or '') self._update_library_inputs()

Latest Blog Posts

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/Miguel0888/mcp-server'

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