using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Timers; using System.Windows; using System.Windows.Threading; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using CtrEditor.ObjetosSim; using System.Diagnostics; using System.Reflection; using System.IO; using System.Windows.Media.Imaging; using System.Windows.Media; using System.Windows.Shapes; using System.Windows.Controls; using CtrEditor.FuncionesBase; using IronPython.Hosting; using Microsoft.Scripting.Hosting; namespace CtrEditor.Services { /// /// Representa una entrada de log con timestamp /// public class DebugLogEntry { public DateTime Timestamp { get; set; } public string Message { get; set; } public string Level { get; set; } public DebugLogEntry(string message, string level = "Info") { Timestamp = DateTime.Now; Message = message ?? string.Empty; Level = level; } public override string ToString() { return $"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {Message}"; } } /// /// Servidor MCP TCP para CtrEditor que permite control remoto de la aplicación /// para debugging y testing usando el protocolo Model Context Protocol /// public class MCPServer : IDisposable { private readonly MainViewModel _mainViewModel; private readonly int _port; private TcpListener _tcpListener; private CancellationTokenSource _cancellationTokenSource; private bool _isRunning; private readonly object _lockObject = new object(); private ScreenshotManager _screenshotManager; // Simulation timing tracking private readonly Stopwatch _simulationStopwatch; private long _totalSimulationMilliseconds; private bool _lastSimulationStatus; // Python execution support private ScriptEngine _pythonEngine; private ScriptScope _pythonScope; private readonly object _pythonLock = new object(); // Circular debug log system private readonly ConcurrentQueue _debugLogBuffer; private readonly object _logLock = new object(); private readonly System.Timers.Timer _logCleanupTimer; private const int MAX_LOG_ENTRIES = 1000; private volatile int _currentLogCount = 0; public MCPServer(MainViewModel mainViewModel, int port = 5006) { _mainViewModel = mainViewModel ?? throw new ArgumentNullException(nameof(mainViewModel)); _port = port; _cancellationTokenSource = new CancellationTokenSource(); // Initialize simulation timing _simulationStopwatch = new Stopwatch(); _totalSimulationMilliseconds = 0; _lastSimulationStatus = false; // Initialize circular debug log system _debugLogBuffer = new ConcurrentQueue(); _logCleanupTimer = new System.Timers.Timer(1000); // 1 second interval _logCleanupTimer.Elapsed += CleanupLogBuffer; _logCleanupTimer.AutoReset = true; _logCleanupTimer.Start(); // Subscribe to debug output from the start Trace.Listeners.Add(new DebugTraceListener(this)); // ScreenshotManager se inicializará de forma lazy cuando se necesite _screenshotManager = null; // Initialize Python environment InitializePythonEnvironment(); } /// /// Obtiene el ScreenshotManager, inicializándolo si es necesario /// private ScreenshotManager GetScreenshotManager() { if (_screenshotManager == null) { var canvas = _mainViewModel?.MainCanvas; if (canvas == null) { throw new InvalidOperationException("Canvas no está disponible. Asegúrate de que la UI esté completamente cargada."); } _screenshotManager = new ScreenshotManager(_mainViewModel, canvas); } return _screenshotManager; } /// /// Inicia el servidor MCP TCP /// public async Task StartAsync() { if (_isRunning) return; try { _tcpListener = new TcpListener(IPAddress.Loopback, _port); _tcpListener.Start(); _isRunning = true; Debug.WriteLine($"[MCP Server] Servidor iniciado en puerto {_port}"); // Procesar conexiones en background _ = Task.Run(async () => await AcceptConnectionsAsync(_cancellationTokenSource.Token)); } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error al iniciar servidor: {ex.Message}"); throw; } } /// /// Detiene el servidor MCP TCP /// public void Stop() { if (!_isRunning) return; lock (_lockObject) { if (!_isRunning) return; _isRunning = false; _cancellationTokenSource?.Cancel(); _tcpListener?.Stop(); Debug.WriteLine("[MCP Server] Servidor detenido"); } } /// /// Acepta conexiones TCP entrantes /// private async Task AcceptConnectionsAsync(CancellationToken cancellationToken) { while (_isRunning && !cancellationToken.IsCancellationRequested) { try { var tcpClient = await _tcpListener.AcceptTcpClientAsync(); Debug.WriteLine("[MCP Server] Nueva conexión aceptada"); // Manejar cliente en tarea separada _ = Task.Run(async () => await HandleClientAsync(tcpClient, cancellationToken)); } catch (ObjectDisposedException) { // Expected when stopping the server break; } catch (Exception ex) { if (_isRunning) { Debug.WriteLine($"[MCP Server] Error aceptando conexión: {ex.Message}"); } } } } /// /// Maneja un cliente TCP individual /// private async Task HandleClientAsync(TcpClient tcpClient, CancellationToken cancellationToken) { try { using (tcpClient) using (var stream = tcpClient.GetStream()) using (var reader = new StreamReader(stream, Encoding.UTF8)) using (var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true }) { Debug.WriteLine("[MCP Server] Cliente conectado"); // No enviar capabilities automáticamente - esperar initialize // await SendInitialCapabilitiesAsync(writer); string line; while ((line = await reader.ReadLineAsync()) != null && !cancellationToken.IsCancellationRequested) { try { if (!string.IsNullOrWhiteSpace(line)) { var response = await ProcessRequestAsync(line); await writer.WriteLineAsync(response); } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error procesando solicitud: {ex.Message}"); var errorResponse = CreateErrorResponse(null, -32603, "Internal error", ex.Message); await writer.WriteLineAsync(errorResponse); } } } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error en cliente: {ex.Message}"); } finally { Debug.WriteLine("[MCP Server] Cliente desconectado"); } } /// /// Envía las capabilities iniciales del servidor MCP /// private async Task SendInitialCapabilitiesAsync(StreamWriter writer) { var capabilities = new { jsonrpc = "2.0", id = "init", result = new { protocolVersion = "2024-11-05", capabilities = new { tools = new object[] { new { name = "list_objects", description = "List all objects in the simulation with metadata" }, new { name = "create_object", description = "Create a new object by type at specified position" }, new { name = "update_object", description = "Update object properties by ID using JSON" }, new { name = "delete_objects", description = "Delete objects by ID (single or batch)" }, new { name = "list_object_types", description = "List all available object types that can be created" }, new { name = "start_simulation", description = "Start the physics simulation" }, new { name = "stop_simulation", description = "Stop the physics simulation" }, new { name = "get_simulation_status", description = "Get current simulation status" }, new { name = "get_plc_status", description = "Get PLC connection status" }, new { name = "take_screenshot", description = "Take a screenshot of the canvas" }, new { name = "take_object_screenshot", description = "Take a screenshot of specific objects by their IDs" }, new { name = "save_project", description = "Save the current project" }, new { name = "reset_simulation_timing", description = "Reset simulation timing counters" }, new { name = "search_debug_log", description = "Search debug log entries with pattern matching" }, new { name = "get_debug_stats", description = "Get debug log buffer statistics" }, new { name = "clear_debug_buffer", description = "Clear the debug log buffer" }, new { name = "execute_python", description = "Execute Python code with access to CtrEditor objects" }, new { name = "python_help", description = "Get help about available Python objects and methods" } } }, serverInfo = new { name = "CtrEditor MCP Server", version = "1.0.0", description = "CtrEditor WPF Application MCP Server for debugging and testing" } } }; var json = JsonConvert.SerializeObject(capabilities); await writer.WriteLineAsync(json); Debug.WriteLine("[MCP Server] Capabilities enviadas"); } /// /// Procesa una solicitud JSON-RPC /// private async Task ProcessRequestAsync(string requestJson) { try { var request = JsonConvert.DeserializeObject(requestJson); var method = request["method"]?.ToString(); var id = request["id"]; var parameters = request["params"] as JObject; Debug.WriteLine($"[MCP Server] Procesando método: {method}"); return method switch { "initialize" => HandleInitializeAsync(id, parameters), "tools/list" => HandleToolsListAsync(id), "tools/call" => await HandleToolCallAsync(id, parameters), _ => CreateErrorResponse(id, -32601, "Method not found", $"Unknown method: {method}") }; } catch (JsonException ex) { return CreateErrorResponse(null, -32700, "Parse error", ex.Message); } catch (Exception ex) { return CreateErrorResponse(null, -32603, "Internal error", ex.Message); } } /// /// Maneja la solicitud initialize /// private string HandleInitializeAsync(JToken id, JObject parameters) { var result = new { protocolVersion = "2024-11-05", capabilities = new { tools = new { }, resources = new { }, prompts = new { } }, serverInfo = new { name = "CtrEditor MCP Server", version = "1.0.0" } }; return CreateSuccessResponse(id, result); } /// /// Maneja la solicitud tools/list /// private string HandleToolsListAsync(JToken id) { var tools = new object[] { new { name = "list_objects", description = "List all objects in the simulation", inputSchema = new { type = "object", properties = new { } } }, new { name = "create_object", description = "Create a new object by type at specified position", inputSchema = new { type = "object", properties = new { type = new { type = "string", description = "Object type to create" }, x = new { type = "number", description = "X position" }, y = new { type = "number", description = "Y position" } }, required = new[] { "type", "x", "y" } } }, new { name = "take_screenshot", description = "Take a screenshot of the canvas. By default captures full canvas without background image and saves to screenshots subdirectory.", inputSchema = new { type = "object", properties = new { filename = new { type = "string", description = "Optional filename for the screenshot (will be saved in screenshots subdirectory). Defaults to timestamp-based name." }, include_background = new { type = "boolean", description = "Whether to include canvas background image. Defaults to false (white background)." }, x = new { type = "number", description = "X coordinate in meters for partial capture (optional)" }, y = new { type = "number", description = "Y coordinate in meters for partial capture (optional)" }, width = new { type = "number", description = "Width in meters for partial capture (optional)" }, height = new { type = "number", description = "Height in meters for partial capture (optional)" }, center_coordinates = new { type = "boolean", description = "If true, x and y parameters represent the center of the capture area instead of top-left corner. Defaults to false for backward compatibility." } } } }, new { name = "take_object_screenshot", description = "Take a screenshot of specific objects by their IDs. This automatically calculates the bounding box of the specified objects and captures only that area with optional padding.", inputSchema = new { type = "object", properties = new { object_ids = new { type = "array", items = new { type = "string" }, description = "Array of object IDs to capture. Can be a single object or multiple objects." }, padding_meters = new { type = "number", description = "Additional padding around objects in meters. Defaults to 0.5 meters." }, filename = new { type = "string", description = "Optional filename for the screenshot. Defaults to timestamp-based name." }, include_background = new { type = "boolean", description = "Whether to include canvas background image. Defaults to false." } }, required = new[] { "object_ids" } } }, new { name = "start_simulation", description = "Start the physics simulation", inputSchema = new { type = "object", properties = new { } } }, new { name = "stop_simulation", description = "Stop the physics simulation", inputSchema = new { type = "object", properties = new { } } }, new { name = "reset_simulation_timing", description = "Reset simulation timing counters to zero", inputSchema = new { type = "object", properties = new { } } }, new { name = "execute_python", description = "Execute Python code with access to CtrEditor objects and canvas for debugging", inputSchema = new { type = "object", properties = new { code = new { type = "string", description = "Python code to execute. Has access to 'app' (MainViewModel), 'canvas', 'objects' (simulable objects), and common libraries." }, return_variables = new { type = "array", items = new { type = "string" }, description = "Optional list of variable names to return from Python execution" }, timeout_seconds = new { type = "number", description = "Execution timeout in seconds. Defaults to 30." } }, required = new[] { "code" } } }, new { name = "python_help", description = "Get help about available Python objects and methods in CtrEditor context", inputSchema = new { type = "object", properties = new { object_name = new { type = "string", description = "Optional specific object to get help for (e.g., 'app', 'canvas', 'objects')" } } } }, new { name = "search_debug_log", description = "Search debug log entries with pattern matching (regex or text)", inputSchema = new { type = "object", properties = new { pattern = new { type = "string", description = "Search pattern (regex or plain text). Empty returns recent entries." }, max_lines = new { type = "number", description = "Maximum number of matching lines to return. Defaults to 100." }, last_n_lines = new { type = "number", description = "Search only in the last N log entries. Defaults to 1000." }, case_sensitive = new { type = "boolean", description = "Whether search should be case sensitive. Defaults to false." } } } }, new { name = "get_debug_stats", description = "Get debug log buffer statistics and status", inputSchema = new { type = "object", properties = new { } } }, new { name = "clear_debug_buffer", description = "Clear the debug log buffer", inputSchema = new { type = "object", properties = new { } } } }; return CreateSuccessResponse(id, new { tools }); } /// /// Crea una respuesta de éxito /// private string CreateSuccessResponse(JToken id, object result) { var response = new { jsonrpc = "2.0", id = id, result = result }; return JsonConvert.SerializeObject(response); } /// /// Maneja llamadas a herramientas (tools/call) /// private async Task HandleToolCallAsync(JToken id, JObject parameters) { try { var toolName = parameters?["name"]?.ToString(); var arguments = parameters?["arguments"] as JObject ?? new JObject(); Debug.WriteLine($"[MCP Server] Ejecutando herramienta: {toolName}"); var result = await Application.Current.Dispatcher.InvokeAsync(() => ExecuteTool(toolName, arguments)); // Envolver el resultado en el formato MCP correcto var mcpResult = new { content = new[] { new { type = "text", text = JsonConvert.SerializeObject(result, Formatting.Indented) } } }; return CreateSuccessResponse(id, mcpResult); } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error ejecutando herramienta: {ex.Message}"); return CreateErrorResponse(id, -32603, "Tool execution error", ex.Message); } } /// /// Ejecuta una herramienta específica (debe ejecutarse en UI thread) /// private object ExecuteTool(string toolName, JObject arguments) { return toolName switch { "list_objects" => ListObjects(), "create_object" => CreateObject(arguments), "update_object" => UpdateObject(arguments), "delete_objects" => DeleteObjects(arguments), "list_object_types" => ListObjectTypes(), "start_simulation" => StartSimulation(arguments), "stop_simulation" => StopSimulation(), "get_simulation_status" => GetSimulationStatus(), "get_plc_status" => GetPlcStatus(), "take_screenshot" => TakeScreenshot(arguments), "take_object_screenshot" => TakeObjectScreenshot(arguments), "save_project" => SaveProject(), "reset_simulation_timing" => ResetSimulationTiming(), "execute_python" => ExecutePython(arguments), "python_help" => GetPythonHelp(arguments), "search_debug_log" => SearchDebugLog(arguments), "get_debug_stats" => GetDebugStats(), "clear_debug_buffer" => ClearDebugBuffer(), _ => throw new ArgumentException($"Unknown tool: {toolName}") }; } /// /// Updates simulation timing based on current status /// private void UpdateSimulationTiming() { var currentSimulationStatus = _mainViewModel.IsSimulationRunning; if (_lastSimulationStatus != currentSimulationStatus) { if (currentSimulationStatus) { // Simulation just started Debug.WriteLine("[MCP Server] Simulation started - starting timer"); _simulationStopwatch.Start(); } else { // Simulation just stopped if (_simulationStopwatch.IsRunning) { _simulationStopwatch.Stop(); _totalSimulationMilliseconds += _simulationStopwatch.ElapsedMilliseconds; Debug.WriteLine($"[MCP Server] Simulation stopped - total time: {_totalSimulationMilliseconds}ms"); _simulationStopwatch.Reset(); } } _lastSimulationStatus = currentSimulationStatus; } } /// /// Gets current simulation elapsed time in milliseconds /// private long GetCurrentSimulationMilliseconds() { UpdateSimulationTiming(); var currentElapsed = _simulationStopwatch.IsRunning ? _simulationStopwatch.ElapsedMilliseconds : 0; return _totalSimulationMilliseconds + currentElapsed; } /// /// Resets simulation timing counters /// private object ResetSimulationTiming() { try { var wasRunning = _simulationStopwatch.IsRunning; var previousTotal = _totalSimulationMilliseconds; var previousCurrent = _simulationStopwatch.ElapsedMilliseconds; _simulationStopwatch.Reset(); _totalSimulationMilliseconds = 0; if (wasRunning && _mainViewModel.IsSimulationRunning) { _simulationStopwatch.Start(); } Debug.WriteLine("[MCP Server] Simulation timing reset"); return new { success = true, message = "Simulation timing reset successfully", previous_total_ms = previousTotal, previous_current_ms = previousCurrent, was_running = wasRunning }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } #region Tool Implementations /// /// Lista todos los objetos en la simulación con metadata /// private object ListObjects() { try { var objects = _mainViewModel.ObjetosSimulables .Where(obj => obj.Show_On_This_Page) .Select(obj => new { id = obj.Id, name = obj.Nombre, type = obj.GetType().Name, position = new { x = obj.Left, y = obj.Top }, dimensions = new { width = obj.Ancho, height = obj.Alto }, angle = obj.Angulo, visible = obj.IsVisFilter, locked = obj.Lock_movement, tags = obj.ListaEtiquetas?.ToArray() ?? new string[0], properties = GetObjectProperties(obj) }) .ToArray(); return new { success = true, count = objects.Length, simulation_elapsed_ms = GetCurrentSimulationMilliseconds(), simulation_running = _mainViewModel.IsSimulationRunning, objects = objects }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Obtiene las propiedades serializables de un objeto /// private object GetObjectProperties(osBase obj) { try { var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto, NullValueHandling = NullValueHandling.Ignore, ReferenceLoopHandling = ReferenceLoopHandling.Ignore }; var json = JsonConvert.SerializeObject(obj, settings); return JsonConvert.DeserializeObject(json); } catch { return new { error = "Could not serialize object properties" }; } } /// /// Crea un nuevo objeto del tipo especificado /// private object CreateObject(JObject arguments) { try { var typeName = arguments["type"]?.ToString(); var x = arguments["x"]?.ToObject() ?? 0f; var y = arguments["y"]?.ToObject() ?? 0f; var properties = arguments["properties"] as JObject; if (string.IsNullOrEmpty(typeName)) throw new ArgumentException("Type is required"); // Buscar el tipo en la lista de tipos disponibles var tipoSimulable = _mainViewModel.ListaOsBase.FirstOrDefault(t => t.Tipo.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase) || t.Nombre.Equals(typeName, StringComparison.OrdinalIgnoreCase)); if (tipoSimulable == null) throw new ArgumentException($"Object type '{typeName}' not found"); // Crear el objeto var newObject = _mainViewModel.CrearObjetoSimulable(tipoSimulable.Tipo, x, y); if (newObject == null) throw new Exception("Failed to create object"); // Aplicar propiedades adicionales si se proporcionaron if (properties != null) { ApplyPropertiesToObject(newObject, properties); } // Crear el UserControl asociado _mainViewModel.CrearUserControlDesdeObjetoSimulable(newObject); return new { success = true, object_id = newObject.Id, message = $"Object of type '{typeName}' created successfully", object_data = new { id = newObject.Id, name = newObject.Nombre, type = newObject.GetType().Name, position = new { x = newObject.Left, y = newObject.Top }, dimensions = new { width = newObject.Ancho, height = newObject.Alto } } }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Actualiza las propiedades de un objeto existente /// private object UpdateObject(JObject arguments) { try { var objectId = arguments["id"]?.ToObject(); var properties = arguments["properties"] as JObject; if (!objectId.HasValue) throw new ArgumentException("Object ID is required"); if (properties == null) throw new ArgumentException("Properties are required"); var obj = _mainViewModel.ObjetosSimulables.FirstOrDefault(o => o.Id.Value == objectId.Value); if (obj == null) throw new ArgumentException($"Object with ID {objectId} not found"); ApplyPropertiesToObject(obj, properties); return new { success = true, object_id = objectId.Value, message = "Object updated successfully" }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Aplica propiedades JSON a un objeto /// private void ApplyPropertiesToObject(osBase obj, JObject properties) { var objType = obj.GetType(); foreach (var prop in properties) { try { var property = objType.GetProperty(prop.Key); if (property != null && property.CanWrite) { var value = prop.Value.ToObject(property.PropertyType); property.SetValue(obj, value); } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error setting property {prop.Key}: {ex.Message}"); } } } /// /// Elimina objetos por ID /// private object DeleteObjects(JObject arguments) { try { var ids = new List(); // Soportar tanto ID único como array de IDs if (arguments["id"] != null) { ids.Add(arguments["id"].ToObject()); } else if (arguments["ids"] != null) { ids.AddRange(arguments["ids"].ToObject()); } else { throw new ArgumentException("Either 'id' or 'ids' is required"); } var deletedCount = 0; var errors = new List(); foreach (var id in ids) { var obj = _mainViewModel.ObjetosSimulables.FirstOrDefault(o => o.Id.Value == id); if (obj != null) { _mainViewModel.RemoverObjetoSimulable(obj); deletedCount++; } else { errors.Add($"Object with ID {id} not found"); } } return new { success = true, deleted_count = deletedCount, errors = errors.ToArray(), message = $"Deleted {deletedCount} object(s)" }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Lista todos los tipos de objetos disponibles para crear /// private object ListObjectTypes() { try { var types = _mainViewModel.ListaOsBase .Select(t => new { name = t.Nombre, type_name = t.Tipo.Name, category = t.Categoria, full_type_name = t.Tipo.FullName }) .OrderBy(t => t.category) .ThenBy(t => t.name) .ToArray(); var categories = types.GroupBy(t => t.category) .Select(g => new { category = g.Key, types = g.ToArray() }) .ToArray(); return new { success = true, total_types = types.Length, categories = categories, all_types = types }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Inicia la simulación /// private object StartSimulation(JObject arguments) { try { var duration = arguments["duration"]?.ToObject(); // Duration in seconds if (_mainViewModel.IsSimulationRunning) { return new { success = false, error = "Simulation is already running" }; } // Iniciar simulación usando el método privado var startMethod = typeof(MainViewModel).GetMethod("StartSimulation", BindingFlags.NonPublic | BindingFlags.Instance); startMethod?.Invoke(_mainViewModel, null); // Si se especifica duración, programar parada automática if (duration.HasValue && duration.Value > 0) { var timer = new System.Timers.Timer(duration.Value * 1000); timer.Elapsed += (s, e) => { timer.Dispose(); Application.Current.Dispatcher.Invoke(() => _mainViewModel.StopSimulation()); }; timer.Start(); } return new { success = true, message = "Simulation started successfully", duration_seconds = duration, auto_stop = duration.HasValue, simulation_elapsed_ms = GetCurrentSimulationMilliseconds() }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Detiene la simulación /// private object StopSimulation() { try { if (!_mainViewModel.IsSimulationRunning) { return new { success = false, error = "Simulation is not running" }; } _mainViewModel.StopSimulation(); return new { success = true, message = "Simulation stopped successfully", simulation_elapsed_ms = GetCurrentSimulationMilliseconds(), simulation_elapsed_seconds = Math.Round(GetCurrentSimulationMilliseconds() / 1000.0, 3) }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Obtiene el estado actual de la simulación /// private object GetSimulationStatus() { try { var elapsedMs = GetCurrentSimulationMilliseconds(); return new { success = true, is_running = _mainViewModel.IsSimulationRunning, simulation_elapsed_ms = elapsedMs, simulation_elapsed_seconds = Math.Round(elapsedMs / 1000.0, 3), object_count = _mainViewModel.ObjetosSimulables.Count, visible_objects = _mainViewModel.ObjetosSimulables.Count(o => o.Show_On_This_Page) }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Obtiene el estado de conexión del PLC /// private object GetPlcStatus() { try { return new { success = true, is_connected = _mainViewModel.IsConnected, plc_enabled = _mainViewModel.PLCViewModel != null }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Toma una captura de pantalla del canvas usando el nuevo ScreenshotManager /// private object TakeScreenshot(JObject arguments) { try { var filename = arguments["filename"]?.ToString(); var includeBackground = arguments["include_background"]?.ToObject() ?? false; var saveFile = arguments["save_file"]?.ToObject() ?? true; var returnBase64 = arguments["return_base64"]?.ToObject() ?? true; var x = arguments["x"]?.ToObject(); var y = arguments["y"]?.ToObject(); var width = arguments["width"]?.ToObject(); var height = arguments["height"]?.ToObject(); var centerCoordinates = arguments["center_coordinates"]?.ToObject() ?? false; // Obtener ScreenshotManager de forma lazy var screenshotManager = GetScreenshotManager(); ScreenshotResult result; if (x.HasValue && y.HasValue && width.HasValue && height.HasValue) { // Captura de área específica if (centerCoordinates) { result = screenshotManager.CaptureCenteredArea( x.Value, y.Value, width.Value, height.Value, filename, includeBackground, saveFile, returnBase64); } else { result = screenshotManager.CaptureArea( x.Value, y.Value, width.Value, height.Value, filename, includeBackground, saveFile, returnBase64); } } else { // Captura de todo el canvas result = screenshotManager.CaptureFullCanvas( filename, includeBackground, saveFile, returnBase64); } if (result.Success) { return new { success = true, filename = result.FileName, full_path = result.FilePath, directory = result.Directory, file_size_bytes = result.FileSizeBytes, timestamp = result.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), message = "Screenshot saved successfully", base64_data = returnBase64 ? result.Base64Data : null, canvas_info = new { canvas_width_pixels = _mainViewModel.MainCanvas?.ActualWidth ?? 0, canvas_height_pixels = _mainViewModel.MainCanvas?.ActualHeight ?? 0, canvas_width_meters = result.CaptureArea?.Width ?? 0, canvas_height_meters = result.CaptureArea?.Height ?? 0 }, capture_info = new { include_background = result.IncludeBackground, area_specified = x.HasValue && y.HasValue && width.HasValue && height.HasValue, area = result.CaptureArea != null ? new { x = Math.Round(result.CaptureArea.Left, 3), y = Math.Round(result.CaptureArea.Top, 3), width = Math.Round(result.CaptureArea.Width, 3), height = Math.Round(result.CaptureArea.Height, 3), center_x = Math.Round(result.CaptureArea.CenterX, 3), center_y = Math.Round(result.CaptureArea.CenterY, 3), units = "meters" } : null, capture_type = result.CaptureType.ToString().ToLower(), center_coordinates = centerCoordinates } }; } else { return new { success = false, error = result.ErrorMessage, error_type = result.ErrorType }; } } catch (Exception ex) { return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } } /// /// Guarda el proyecto actual /// private object SaveProject() { try { _mainViewModel.Save(); return new { success = true, message = "Project saved successfully" }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Toma una captura de pantalla de objetos específicos por sus IDs usando el nuevo ScreenshotManager /// private object TakeObjectScreenshot(JObject arguments) { try { var objectIds = arguments["object_ids"]?.ToObject(); if (objectIds == null || objectIds.Length == 0) throw new ArgumentException("object_ids is required and must not be empty"); var paddingMeters = arguments["padding_meters"]?.ToObject() ?? 0.5f; var filename = arguments["filename"]?.ToString(); var includeBackground = arguments["include_background"]?.ToObject() ?? false; var saveFile = arguments["save_file"]?.ToObject() ?? true; var returnBase64 = arguments["return_base64"]?.ToObject() ?? true; // Obtener ScreenshotManager de forma lazy y usar el nuevo método var screenshotManager = GetScreenshotManager(); var result = screenshotManager.CaptureObjects( objectIds, paddingMeters, filename, includeBackground, saveFile, returnBase64); if (result.Success) { return new { success = true, filename = result.FileName, full_path = result.FilePath, directory = result.Directory, file_size_bytes = result.FileSizeBytes, timestamp = result.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"), message = "Object screenshot saved successfully", base64_data = returnBase64 ? result.Base64Data : null, captured_objects = result.CapturedObjects.Select(obj => new { id = obj.Id, name = obj.Name, type = obj.Type, position = new { x = obj.Left, y = obj.Top }, center = new { x = obj.CenterX, y = obj.CenterY }, dimensions = new { width = obj.Width, height = obj.Height } }).ToArray(), bounding_box = new { left = Math.Round(result.BoundingBox.Left, 3), top = Math.Round(result.BoundingBox.Top, 3), right = Math.Round(result.BoundingBox.Left + result.BoundingBox.Width, 3), bottom = Math.Round(result.BoundingBox.Top + result.BoundingBox.Height, 3), center_x = Math.Round(result.BoundingBox.CenterX, 3), center_y = Math.Round(result.BoundingBox.CenterY, 3), width = Math.Round(result.BoundingBox.Width, 3), height = Math.Round(result.BoundingBox.Height, 3) }, capture_area = new { left = Math.Round(result.CaptureArea.Left, 3), top = Math.Round(result.CaptureArea.Top, 3), center_x = Math.Round(result.CaptureArea.CenterX, 3), center_y = Math.Round(result.CaptureArea.CenterY, 3), width = Math.Round(result.CaptureArea.Width, 3), height = Math.Round(result.CaptureArea.Height, 3), padding_meters = result.PaddingMeters }, capture_info = new { include_background = result.IncludeBackground, capture_type = result.CaptureType.ToString().ToLower(), objects_count = result.CapturedObjects.Count } }; } else { return new { success = false, error = result.ErrorMessage, error_type = result.ErrorType }; } } catch (Exception ex) { return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } } #endregion #region Python Execution Support /// /// Initializes the Python environment with enhanced libraries and thread-safe print function /// private void InitializePythonEnvironment() { try { // Set console output encoding to avoid codepage issues try { Console.OutputEncoding = System.Text.Encoding.UTF8; } catch { // Ignore encoding setup errors } _pythonEngine = Python.CreateEngine(); _pythonScope = _pythonEngine.CreateScope(); // Set up enhanced search paths for IronPython.StdLib var searchPaths = _pythonEngine.GetSearchPaths(); // Add current directory and common library paths var currentDir = Directory.GetCurrentDirectory(); searchPaths.Add(currentDir); searchPaths.Add(System.IO.Path.Combine(currentDir, "lib")); searchPaths.Add(System.IO.Path.Combine(currentDir, "Lib")); _pythonEngine.SetSearchPaths(searchPaths); // Import basic libraries and set up global variables var setupScript = @" import sys # Fix encoding issues before importing anything else try: import codecs # Override the problematic codepage lookup def search_function(encoding): if 'codepage' in encoding.lower(): return codecs.lookup('utf-8') return None codecs.register(search_function) except: pass import clr import math import time import json import random # Add .NET types clr.AddReference('System') clr.AddReference('System.Core') clr.AddReference('PresentationCore') clr.AddReference('PresentationFramework') from System import Console, Text from System.Text import StringBuilder # Create completely isolated print system to avoid encoding issues _print_buffer = StringBuilder() def safe_print(*args, **kwargs): '''Completely isolated print function that avoids all encoding issues''' try: separator = kwargs.get('sep', ' ') end_char = kwargs.get('end', '\n') # Convert all arguments to strings safely text_parts = [] for arg in args: try: text_parts.append(str(arg)) except: text_parts.append('') text = separator.join(text_parts) + end_char # Store in our isolated buffer _print_buffer.Append(text) # Also write to debug output for monitoring try: import System.Diagnostics System.Diagnostics.Debug.WriteLine('[Python] ' + text.rstrip()) except: pass except Exception as e: try: _print_buffer.Append(f'Print error: {e}\n') except: pass # Completely replace print function - no fallback to original print = safe_print # Helper function to get print output def get_print_output(): '''Get accumulated print output and clear buffer''' try: output = _print_buffer.ToString() _print_buffer.Clear() return output except: return '' def get_objects(): '''Helper function to get all simulable objects as a list''' try: return list(objects) if objects else [] except: return [] "; _pythonEngine.Execute(setupScript, _pythonScope); Debug.WriteLine("[MCP Server] Python environment initialized successfully"); } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error initializing Python environment: {ex.Message}"); Debug.WriteLine($"[MCP Server] Stack trace: {ex.StackTrace}"); } } /// /// Executes Python code with access to CtrEditor objects /// private object ExecutePython(JObject arguments) { lock (_pythonLock) { try { var code = arguments["code"]?.ToString(); if (string.IsNullOrEmpty(code)) throw new ArgumentException("Code is required"); var returnVariables = arguments["return_variables"]?.ToObject() ?? new string[0]; var timeoutSeconds = arguments["timeout_seconds"]?.ToObject() ?? 30; // Set up context variables with thread-safe access return Application.Current.Dispatcher.Invoke(() => { try { // Set global variables for Python script _pythonScope.SetVariable("app", _mainViewModel); _pythonScope.SetVariable("canvas", _mainViewModel.MainCanvas); _pythonScope.SetVariable("objects", _mainViewModel.ObjetosSimulables); // Execute the Python code directly on UI thread to avoid cross-thread issues // Note: This runs synchronously on UI thread but IronPython is generally fast _pythonEngine.Execute(code, _pythonScope); // Get print output var printOutput = ""; try { var getPrintOutput = _pythonScope.GetVariable("get_print_output"); if (getPrintOutput != null) { var result = _pythonEngine.Operations.Invoke(getPrintOutput); printOutput = result?.ToString() ?? ""; } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error getting print output: {ex.Message}"); } // Collect return variables var returnValues = new Dictionary(); foreach (var varName in returnVariables) { try { if (_pythonScope.ContainsVariable(varName)) { var value = _pythonScope.GetVariable(varName); returnValues[varName] = ConvertPythonObject(value); } else { returnValues[varName] = null; } } catch (Exception ex) { returnValues[varName] = $"Error getting variable: {ex.Message}"; } } return new { success = true, output = printOutput, variables = returnValues, execution_time_ms = "< 1000" }; } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Python execution error: {ex.Message}"); return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } }); } catch (Exception ex) { return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } } } /// /// Converts Python objects to JSON-serializable .NET objects with better type handling /// private object ConvertPythonObject(dynamic pythonObj) { if (pythonObj == null) return null; try { // Handle basic .NET types if (pythonObj is string || pythonObj is int || pythonObj is double || pythonObj is bool || pythonObj is decimal) { return pythonObj; } // Handle System.Single (float) conversion if (pythonObj is float || pythonObj.GetType() == typeof(System.Single)) { return Convert.ToDouble(pythonObj); } // Handle nullable types if (pythonObj.GetType().IsGenericType && pythonObj.GetType().GetGenericTypeDefinition() == typeof(Nullable<>)) { var underlyingValue = pythonObj.HasValue ? pythonObj.Value : null; return underlyingValue != null ? ConvertPythonObject(underlyingValue) : null; } // Handle collections if (pythonObj is System.Collections.IEnumerable enumerable && !(pythonObj is string)) { var list = new List(); foreach (var item in enumerable) { list.Add(ConvertPythonObject(item)); } return list; } // Handle objects with simple properties var type = pythonObj.GetType(); if (type.IsClass && !type.FullName.StartsWith("System.")) { var properties = new Dictionary(); var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); var validProps = new List(); // Filter properties manually to avoid lambda issues foreach (var prop in allProps) { if (prop.CanRead && prop.GetIndexParameters().Length == 0) { validProps.Add(prop); if (validProps.Count >= 20) // Limit to prevent infinite recursion break; } } foreach (var prop in validProps) { try { var value = prop.GetValue(pythonObj); properties[prop.Name] = ConvertPythonObject(value); } catch { properties[prop.Name] = $""; } } return properties; } // Fallback: convert to string return pythonObj.ToString(); } catch (Exception ex) { return $""; } } /// /// Gets help information about available Python objects and methods /// private object GetPythonHelp(JObject arguments) { try { var objectName = arguments["object_name"]?.ToString(); var helpInfo = new Dictionary { ["available_objects"] = new { app = "MainViewModel - Main application view model with all CtrEditor functionality", canvas = "Canvas - Main canvas where objects are displayed", objects = "ObservableCollection - Collection of all simulable objects", get_objects = "Function() - Helper function that returns objects as a Python list" }, ["available_libraries"] = new[] { "sys - System-specific parameters and functions", "math - Mathematical functions", "time - Time-related functions", "json - JSON encoder and decoder", "random - Random number generation", "clr - .NET CLR integration" }, ["common_usage_patterns"] = new[] { "len(objects) - Get number of objects", "objects[0].Id.Value - Get ID of first object", "app.IsSimulationRunning - Check if simulation is running", "canvas.Width, canvas.Height - Get canvas dimensions", "print('Hello') - Print to output (thread-safe)" } }; if (!string.IsNullOrEmpty(objectName)) { Application.Current.Dispatcher.Invoke(() => { try { object targetObject = objectName.ToLower() switch { "app" => _mainViewModel, "canvas" => _mainViewModel.MainCanvas, "objects" => _mainViewModel.ObjetosSimulables, _ => null }; if (targetObject != null) { var type = targetObject.GetType(); var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => !m.IsSpecialName && m.DeclaringType != typeof(object)) .Take(20) .Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"))})") .ToArray(); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead) .Take(20) .Select(p => $"{p.Name} : {p.PropertyType.Name}") .ToArray(); helpInfo[$"{objectName}_methods"] = methods; helpInfo[$"{objectName}_properties"] = properties; } } catch (Exception ex) { helpInfo["error"] = $"Error getting help for {objectName}: {ex.Message}"; } }); } return new { success = true, help = helpInfo }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } #endregion #region Circular Debug Log System /// /// Custom TraceListener for capturing debug output /// private class DebugTraceListener : TraceListener { private readonly MCPServer _server; public DebugTraceListener(MCPServer server) { _server = server; } public override void Write(string message) { if (!string.IsNullOrEmpty(message)) { _server.AddLogEntry(message, "Debug"); } } public override void WriteLine(string message) { if (!string.IsNullOrEmpty(message)) { _server.AddLogEntry(message, "Debug"); } } } /// /// Adds a log entry to the circular buffer /// public void AddLogEntry(string message, string level = "Info") { if (string.IsNullOrEmpty(message)) return; var entry = new DebugLogEntry(message, level); _debugLogBuffer.Enqueue(entry); Interlocked.Increment(ref _currentLogCount); } /// /// Timer callback to cleanup old log entries /// private void CleanupLogBuffer(object sender, System.Timers.ElapsedEventArgs e) { lock (_logLock) { while (_currentLogCount > MAX_LOG_ENTRIES && _debugLogBuffer.TryDequeue(out _)) { Interlocked.Decrement(ref _currentLogCount); } } } /// /// Search debug log with pattern matching /// private object SearchDebugLog(JObject arguments) { try { var pattern = arguments?["pattern"]?.ToString() ?? ""; var maxLines = arguments?["max_lines"]?.ToObject() ?? 100; var lastNLines = arguments?["last_n_lines"]?.ToObject() ?? 1000; var caseSensitive = arguments?["case_sensitive"]?.ToObject() ?? false; // Get current log entries in chronological order var logEntries = _debugLogBuffer.ToArray(); // Take only the last N entries if specified var entriesToSearch = logEntries.TakeLast(Math.Min(lastNLines, logEntries.Length)).ToArray(); var matchingEntries = new List(); if (string.IsNullOrEmpty(pattern)) { // Return all entries if no pattern specified matchingEntries.AddRange(entriesToSearch.TakeLast(maxLines)); } else { // Use regex for pattern matching try { var regexOptions = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase; var regex = new Regex(pattern, regexOptions); foreach (var entry in entriesToSearch) { if (regex.IsMatch(entry.Message)) { matchingEntries.Add(entry); if (matchingEntries.Count >= maxLines) break; } } } catch (ArgumentException) { // If regex fails, fall back to simple text search var comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; foreach (var entry in entriesToSearch) { if (entry.Message.IndexOf(pattern, comparison) >= 0) { matchingEntries.Add(entry); if (matchingEntries.Count >= maxLines) break; } } } } return new { success = true, matches = matchingEntries.Select(entry => new { timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"), level = entry.Level, message = entry.Message }).ToArray(), total_matches = matchingEntries.Count, total_log_entries = _currentLogCount, pattern = pattern, searched_last_n_lines = entriesToSearch.Length }; } catch (Exception ex) { return new { success = false, error = ex.Message }; } } /// /// Get debug log statistics /// private object GetDebugStats() { return new { success = true, current_log_count = _currentLogCount, max_log_entries = MAX_LOG_ENTRIES, is_buffer_full = _currentLogCount >= MAX_LOG_ENTRIES, cleanup_timer_enabled = _logCleanupTimer?.Enabled ?? false }; } /// /// Clear the debug log buffer /// private object ClearDebugBuffer() { lock (_logLock) { while (_debugLogBuffer.TryDequeue(out _)) { Interlocked.Decrement(ref _currentLogCount); } return new { success = true, message = "Debug log buffer cleared" }; } } #endregion #region Helper Methods /// /// Crea una respuesta de error JSON-RPC /// private string CreateErrorResponse(JToken id, int code, string message, string data = null) { var error = new { code = code, message = message, data = data }; var response = new { jsonrpc = "2.0", id = id, error = error }; return JsonConvert.SerializeObject(response); } #endregion #region IDisposable Implementation public void Dispose() { Stop(); _cancellationTokenSource?.Dispose(); // Clean up debug log timer try { _logCleanupTimer?.Stop(); _logCleanupTimer?.Dispose(); } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error disposing log cleanup timer: {ex.Message}"); } // Clean up Python resources try { // ScriptScope doesn't have Dispose, just clear variables _pythonScope?.RemoveVariable("app"); _pythonScope?.RemoveVariable("canvas"); _pythonScope?.RemoveVariable("objects"); _pythonEngine?.Runtime?.Shutdown(); } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error disposing Python resources: {ex.Message}"); } } #endregion } }