#!/usr/bin/env python3 """ MCP Server Configuration and Management This module provides utilities to configure and manage the MCP server for LLM_Train models. """ import json import os import subprocess import sys import tkinter as tk from tkinter import ttk, messagebox, filedialog from typing import Dict, Any, Optional from pathlib import Path class MCPServerConfig: """Configuration management for the MCP server.""" def __init__(self, config_file: str = "mcp_config.json"): self.config_file = config_file self.default_config = { "server": { "name": "llm-train-models", "version": "1.0.0", "description": "Multi-model MCP server for LLM_Train", "host": "localhost", "port": 8000, "auto_start": False }, "models": { "cache_size": 3, "default_context": 4096, "default_gpu_layers": 0 }, "logging": { "level": "INFO", "file": "mcp_server.log" } } self.config = self.load_config() def load_config(self) -> Dict[str, Any]: """Load configuration from file.""" try: if os.path.exists(self.config_file): with open(self.config_file, 'r') as f: loaded = json.load(f) # Merge with defaults return self._merge_config(self.default_config, loaded) return self.default_config.copy() except Exception as e: print(f"Error loading MCP config: {e}") return self.default_config.copy() def _merge_config(self, defaults: Dict, loaded: Dict) -> Dict: """Merge loaded config with defaults.""" result = defaults.copy() for key, value in loaded.items(): if key in result and isinstance(result[key], dict) and isinstance(value, dict): result[key] = self._merge_config(result[key], value) else: result[key] = value return result def save_config(self) -> bool: """Save configuration to file.""" try: with open(self.config_file, 'w') as f: json.dump(self.config, f, indent=2) return True except Exception as e: print(f"Error saving MCP config: {e}") return False def get(self, path: str, default: Any = None) -> Any: """Get config value using dot notation.""" keys = path.split('.') value = self.config for key in keys: if isinstance(value, dict) and key in value: value = value[key] else: return default return value def set(self, path: str, value: Any): """Set config value using dot notation.""" keys = path.split('.') target = self.config for key in keys[:-1]: if key not in target: target[key] = {} target = target[key] target[keys[-1]] = value def generate_claude_config(self) -> Dict[str, Any]: """Generate Claude Desktop MCP configuration.""" script_path = os.path.abspath("mcp_server.py") python_path = sys.executable return { "mcpServers": { "llm-train-models": { "command": python_path, "args": [script_path], "env": { "PYTHONPATH": os.getcwd() } } } } class MCPConfigGUI: """GUI for configuring the MCP server.""" def __init__(self, parent: tk.Tk): self.parent = parent self.config = MCPServerConfig() self.dialog = tk.Toplevel(parent) self.dialog.title("MCP Server Configuration") self.dialog.geometry("600x500") self.dialog.transient(parent) self.dialog.grab_set() self._build_ui() self._load_values() def _build_ui(self): """Build the configuration UI.""" notebook = ttk.Notebook(self.dialog) notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Server Settings Tab server_frame = ttk.Frame(notebook) notebook.add(server_frame, text="Server") self._build_server_tab(server_frame) # Model Settings Tab model_frame = ttk.Frame(notebook) notebook.add(model_frame, text="Models") self._build_models_tab(model_frame) # Claude Integration Tab claude_frame = ttk.Frame(notebook) notebook.add(claude_frame, text="Claude Integration") self._build_claude_tab(claude_frame) # Buttons button_frame = ttk.Frame(self.dialog) button_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) ttk.Button(button_frame, text="Save", command=self._save_config).pack(side=tk.RIGHT, padx=5) ttk.Button(button_frame, text="Cancel", command=self.dialog.destroy).pack(side=tk.RIGHT) ttk.Button(button_frame, text="Test Server", command=self._test_server).pack(side=tk.LEFT) def _build_server_tab(self, parent: ttk.Frame): """Build server settings tab.""" frame = ttk.LabelFrame(parent, text="Server Configuration", padding=10) frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Server name ttk.Label(frame, text="Server Name:").grid(row=0, column=0, sticky=tk.W, pady=5) self.server_name_var = tk.StringVar() ttk.Entry(frame, textvariable=self.server_name_var, width=30).grid(row=0, column=1, sticky=tk.W, pady=5) # Auto start self.auto_start_var = tk.BooleanVar() ttk.Checkbutton(frame, text="Auto-start server with application", variable=self.auto_start_var).grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=10) # Logging level ttk.Label(frame, text="Logging Level:").grid(row=2, column=0, sticky=tk.W, pady=5) self.log_level_var = tk.StringVar() ttk.Combobox(frame, textvariable=self.log_level_var, values=["DEBUG", "INFO", "WARNING", "ERROR"], state="readonly", width=15).grid(row=2, column=1, sticky=tk.W, pady=5) # Log file ttk.Label(frame, text="Log File:").grid(row=3, column=0, sticky=tk.W, pady=5) log_frame = ttk.Frame(frame) log_frame.grid(row=3, column=1, sticky=tk.W, pady=5) self.log_file_var = tk.StringVar() ttk.Entry(log_frame, textvariable=self.log_file_var, width=25).pack(side=tk.LEFT) ttk.Button(log_frame, text="Browse", command=self._browse_log_file).pack(side=tk.LEFT, padx=5) def _build_models_tab(self, parent: ttk.Frame): """Build model settings tab.""" frame = ttk.LabelFrame(parent, text="Model Configuration", padding=10) frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Cache size ttk.Label(frame, text="Model Cache Size:").grid(row=0, column=0, sticky=tk.W, pady=5) self.cache_size_var = tk.IntVar() cache_spin = tk.Spinbox(frame, from_=1, to=10, textvariable=self.cache_size_var, width=10) cache_spin.grid(row=0, column=1, sticky=tk.W, pady=5) ttk.Label(frame, text="models").grid(row=0, column=2, sticky=tk.W, padx=5) # Default context ttk.Label(frame, text="Default Context Size:").grid(row=1, column=0, sticky=tk.W, pady=5) self.default_ctx_var = tk.IntVar() ctx_spin = tk.Spinbox(frame, from_=512, to=32768, increment=512, textvariable=self.default_ctx_var, width=10) ctx_spin.grid(row=1, column=1, sticky=tk.W, pady=5) ttk.Label(frame, text="tokens").grid(row=1, column=2, sticky=tk.W, padx=5) # Default GPU layers ttk.Label(frame, text="Default GPU Layers:").grid(row=2, column=0, sticky=tk.W, pady=5) self.default_gpu_var = tk.IntVar() gpu_spin = tk.Spinbox(frame, from_=0, to=100, textvariable=self.default_gpu_var, width=10) gpu_spin.grid(row=2, column=1, sticky=tk.W, pady=5) ttk.Label(frame, text="layers").grid(row=2, column=2, sticky=tk.W, padx=5) # Info info_text = ("Cache size determines how many models can be loaded simultaneously.\\n" "Default settings are used when loading models via MCP.") ttk.Label(frame, text=info_text, foreground="gray").grid(row=3, column=0, columnspan=3, pady=10) def _build_claude_tab(self, parent: ttk.Frame): """Build Claude integration tab.""" frame = ttk.LabelFrame(parent, text="Claude Desktop Integration", padding=10) frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Instructions - wrapped properly instructions_frame = ttk.Frame(frame) instructions_frame.pack(fill=tk.X, pady=(0, 10)) # Create a text widget for instructions with proper wrapping instructions_text = tk.Text(instructions_frame, height=8, wrap=tk.WORD, background="#f0f0f0", relief=tk.FLAT, borderwidth=0, state=tk.DISABLED) instructions_text.pack(fill=tk.X) # Add properly formatted instructions instructions_content = """To use this MCP server with Claude Desktop, follow these steps: 1. Locate your Claude Desktop configuration file: • Windows: %APPDATA%\\Claude\\claude_desktop_config.json • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json • Linux: ~/.config/Claude/claude_desktop_config.json 2. Open the configuration file in a text editor (create it if it doesn't exist) 3. Add the JSON configuration shown below to the file 4. Save the file and restart Claude Desktop 5. The LLM_Train models server will be available in Claude Desktop""" instructions_text.config(state=tk.NORMAL) instructions_text.insert(1.0, instructions_content) instructions_text.config(state=tk.DISABLED) # Config display ttk.Label(frame, text="Configuration JSON:", font=("TkDefaultFont", 10, "bold")).pack(anchor=tk.W, pady=(10,5)) # Create text frame with proper layout text_frame = ttk.Frame(frame) text_frame.pack(fill=tk.BOTH, expand=True) # Make the config text read-only and styled self.config_text = tk.Text(text_frame, height=12, width=70, wrap=tk.WORD, state=tk.DISABLED, background="#f8f8f8", font=("Courier", 9)) config_scroll = ttk.Scrollbar(text_frame, orient="vertical", command=self.config_text.yview) self.config_text.configure(yscrollcommand=config_scroll.set) self.config_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) config_scroll.pack(side=tk.RIGHT, fill=tk.Y) # Buttons button_frame = ttk.Frame(frame) button_frame.pack(fill=tk.X, pady=10) ttk.Button(button_frame, text="Copy to Clipboard", command=self._copy_config).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="Save to File", command=self._save_claude_config).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text="Refresh", command=self._update_claude_config).pack(side=tk.LEFT, padx=5) self._update_claude_config() def _load_values(self): """Load configuration values into UI.""" self.server_name_var.set(self.config.get('server.name', 'llm-train-models')) self.auto_start_var.set(self.config.get('server.auto_start', False)) self.log_level_var.set(self.config.get('logging.level', 'INFO')) self.log_file_var.set(self.config.get('logging.file', 'mcp_server.log')) self.cache_size_var.set(self.config.get('models.cache_size', 3)) self.default_ctx_var.set(self.config.get('models.default_context', 4096)) self.default_gpu_var.set(self.config.get('models.default_gpu_layers', 0)) def _save_config(self): """Save configuration.""" self.config.set('server.name', self.server_name_var.get()) self.config.set('server.auto_start', self.auto_start_var.get()) self.config.set('logging.level', self.log_level_var.get()) self.config.set('logging.file', self.log_file_var.get()) self.config.set('models.cache_size', self.cache_size_var.get()) self.config.set('models.default_context', self.default_ctx_var.get()) self.config.set('models.default_gpu_layers', self.default_gpu_var.get()) if self.config.save_config(): messagebox.showinfo("Success", "Configuration saved successfully!") self.dialog.destroy() else: messagebox.showerror("Error", "Failed to save configuration") def _browse_log_file(self): """Browse for log file location.""" filename = filedialog.asksaveasfilename( title="Select Log File", defaultextension=".log", filetypes=[("Log files", "*.log"), ("All files", "*.*")] ) if filename: self.log_file_var.set(filename) def _update_claude_config(self): """Update the Claude configuration display.""" config = self.config.generate_claude_config() config_json = json.dumps(config, indent=2) self.config_text.delete(1.0, tk.END) self.config_text.insert(1.0, config_json) def _copy_config(self): """Copy configuration to clipboard.""" config_text = self.config_text.get(1.0, tk.END) self.dialog.clipboard_clear() self.dialog.clipboard_append(config_text) messagebox.showinfo("Copied", "Configuration copied to clipboard!") def _save_claude_config(self): """Save Claude configuration to file.""" filename = filedialog.asksaveasfilename( title="Save Claude Config", defaultextension=".json", filetypes=[("JSON files", "*.json"), ("All files", "*.*")], initialvalue="claude_desktop_config.json" ) if filename: try: config = self.config.generate_claude_config() with open(filename, 'w') as f: json.dump(config, f, indent=2) messagebox.showinfo("Saved", f"Configuration saved to {filename}") except Exception as e: messagebox.showerror("Error", f"Failed to save file: {e}") def _test_server(self): """Test the MCP server.""" try: # Try to run the server with --help to test if it's working result = subprocess.run([ sys.executable, "mcp_server.py", "--help" ], capture_output=True, text=True, timeout=10) if result.returncode == 0: messagebox.showinfo("Test Successful", "MCP server appears to be working correctly!") else: messagebox.showerror("Test Failed", f"Server test failed:\\n{result.stderr}") except Exception as e: messagebox.showerror("Test Error", f"Failed to test server: {e}") def open_mcp_config(parent: tk.Tk): """Open the MCP configuration dialog.""" MCPConfigGUI(parent) if __name__ == "__main__": # Test the configuration root = tk.Tk() root.withdraw() # Hide main window app = MCPConfigGUI(root) root.mainloop()