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 CtrEditor.HydraulicSimulator.Python; 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; // 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; } /// /// 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}"); AddDebugLogEntry($"[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}"); AddDebugLogEntry($"[MCP Server] Error al iniciar servidor: {ex.Message}", "Error"); 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"); AddDebugLogEntry("[MCP Server] Servidor detenido"); } } /// /// Verifica si el dispatcher está disponible sin bloquear /// private bool IsDispatcherAvailable() { try { return Application.Current != null && Application.Current.Dispatcher != null && !Application.Current.Dispatcher.HasShutdownStarted; } catch { return false; } } /// /// Ejecuta una acción en el dispatcher de forma segura con timeout /// private async Task SafeDispatcherInvokeAsync(Func action, int timeoutMs = 5000) { if (!IsDispatcherAvailable()) { throw new InvalidOperationException("Dispatcher no está disponible"); } try { var task = Application.Current.Dispatcher.InvokeAsync(action); return await task.Task.ConfigureAwait(false); } catch (TaskCanceledException) { throw new TimeoutException($"Operación en dispatcher excedió timeout de {timeoutMs}ms"); } } /// /// 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}"); AddDebugLogEntry($"[MCP Server] Ejecutando herramienta: {toolName}"); object result; // Handle async tools that need special execution context if (toolName == "execute_python") { result = await ExecutePythonAsync(arguments); } else { // Use Dispatcher.InvokeAsync with timeout to prevent freezing try { var task = Application.Current.Dispatcher.InvokeAsync(() => ExecuteToolAsync(toolName, arguments)); result = await task.Task.ConfigureAwait(false); } catch (TaskCanceledException) { AddDebugLogEntry($"[MCP Server] Timeout ejecutando herramienta: {toolName}", "Warning"); result = new { success = false, error = "Operation timed out", tool = toolName }; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error ejecutando herramienta {toolName}: {ex.Message}", "Error"); result = new { success = false, error = ex.Message, tool = toolName }; } } // 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 ExecuteToolAsync(string toolName, JObject arguments) { switch (toolName) { case "list_objects": return ListObjects(); case "create_object": return CreateObject(arguments); case "update_object": return UpdateObject(arguments); case "delete_objects": return DeleteObjects(arguments); case "list_object_types": return ListObjectTypes(); case "start_simulation": return StartSimulation(arguments); case "stop_simulation": return StopSimulation(); case "get_simulation_status": return GetSimulationStatus(); case "get_plc_status": return GetPlcStatus(); case "take_screenshot": return TakeScreenshot(arguments); case "take_object_screenshot": return TakeObjectScreenshot(arguments); case "save_project": return SaveProject(); case "reset_simulation_timing": return ResetSimulationTiming(); case "python_help": return GetPythonHelp(arguments); case "search_debug_log": return SearchDebugLog(arguments); case "get_debug_stats": return GetDebugStats(); case "clear_debug_buffer": return ClearDebugBuffer(); default: 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 { if (!IsDispatcherAvailable()) { return new { success = false, error = "CtrEditor not available" }; } object[] objects; bool isRunning; try { 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() ?? new object[0]; isRunning = _mainViewModel.IsSimulationRunning; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error accediendo objetos de simulación: {ex.Message}", "Warning"); return new { success = false, error = "CtrEditor not available" }; } return new { success = true, count = objects.Length, simulation_elapsed_ms = GetCurrentSimulationMilliseconds(), simulation_running = isRunning, objects = objects }; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error en ListObjects: {ex.Message}", "Error"); 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 { if (!IsDispatcherAvailable()) { return new { success = false, error = "CtrEditor not available" }; } var duration = arguments["duration"]?.ToObject(); // Duration in seconds bool isRunning; try { isRunning = _mainViewModel.IsSimulationRunning; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error accediendo estado de simulación: {ex.Message}", "Warning"); return new { success = false, error = "CtrEditor not available" }; } if (isRunning) { return new { success = false, error = "Simulation is already running" }; } // Use BeginInvoke to start simulation without blocking Application.Current.Dispatcher.BeginInvoke(new Action(() => { // Iniciar simulación usando el método privado var startMethod = typeof(MainViewModel).GetMethod("StartSimulation", BindingFlags.NonPublic | BindingFlags.Instance); startMethod?.Invoke(_mainViewModel, null); }), DispatcherPriority.Normal); // 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 += async (s, e) => { timer.Dispose(); try { // Use BeginInvoke instead of Invoke to prevent blocking Application.Current.Dispatcher.BeginInvoke(new Action(() => { _mainViewModel.StopSimulation(); }), DispatcherPriority.Normal); } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error deteniendo simulación automáticamente: {ex.Message}", "Error"); } }; 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 (!IsDispatcherAvailable()) { return new { success = false, error = "CtrEditor not available" }; } bool isRunning; try { isRunning = _mainViewModel.IsSimulationRunning; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error accediendo estado de simulación: {ex.Message}", "Warning"); return new { success = false, error = "CtrEditor not available" }; } if (!isRunning) { return new { success = false, error = "Simulation is not running" }; } // Use BeginInvoke instead of Invoke to prevent blocking Application.Current.Dispatcher.BeginInvoke(new Action(() => { _mainViewModel.StopSimulation(); }), DispatcherPriority.Normal); 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 { // Use thread-safe access to properties if (!IsDispatcherAvailable()) { return new { success = false, error = "CtrEditor not available" }; } var elapsedMs = GetCurrentSimulationMilliseconds(); bool isRunning; int objectCount; int visibleObjects; // Access UI properties safely try { isRunning = _mainViewModel.IsSimulationRunning; objectCount = _mainViewModel.ObjetosSimulables?.Count ?? 0; visibleObjects = _mainViewModel.ObjetosSimulables?.Count(o => o.Show_On_This_Page) ?? 0; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error accediendo propiedades de simulación: {ex.Message}", "Warning"); return new { success = false, error = "CtrEditor not available" }; } return new { success = true, is_running = isRunning, simulation_elapsed_ms = elapsedMs, simulation_elapsed_seconds = Math.Round(elapsedMs / 1000.0, 3), object_count = objectCount, visible_objects = visibleObjects }; } catch (Exception ex) { AddDebugLogEntry($"[MCP Server] Error en GetSimulationStatus: {ex.Message}", "Error"); 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 /// /// Executes Python code using the shared CPython environment from TSNet /// private async Task ExecutePythonAsync(JObject arguments) { 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; // Note: Using process-based CPython execution (no DLL initialization needed) Debug.WriteLine("[MCP Server] Executing Python script via process-based CPython"); // Prepare enhanced script with global variables and helpers var enhancedScript = $@" # Set up CtrEditor context variables import sys import json import math import time # Helper functions def get_objects(): '''Helper function to get all simulable objects as a list''' # Note: In CPython mode, direct object access is limited print('Note: get_objects() - Direct object access not available in CPython mode') return [] def safe_print(*args, **kwargs): '''Safe print function''' try: print(*args, **kwargs) except: pass # Override print with safe version print = safe_print # User code starts here {code} # Collect return variables if specified return_data = {{}} {string.Join("\n", returnVariables.Select(var => $"try:\n return_data['{var}'] = {var}\nexcept:\n return_data['{var}'] = None"))} # Output return data as JSON if return_data: print('RETURN_DATA:' + json.dumps(return_data)) "; // Create temporary script file and execute var tempScript = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"mcp_python_{Guid.NewGuid()}.py"); try { await File.WriteAllTextAsync(tempScript, enhancedScript); var result = await PythonInterop.ExecuteScriptAsync(tempScript); if (result.Success) { // Parse return variables from output if available var returnValues = new Dictionary(); // Try to extract return_data from Python output if (returnVariables.Length > 0 && result.Output.Contains("RETURN_DATA:")) { try { var jsonStart = result.Output.IndexOf("RETURN_DATA:") + "RETURN_DATA:".Length; var jsonEnd = result.Output.IndexOf("\n", jsonStart); if (jsonEnd == -1) jsonEnd = result.Output.Length; var jsonStr = result.Output.Substring(jsonStart, jsonEnd - jsonStart).Trim(); var parsedData = JsonConvert.DeserializeObject>(jsonStr); if (parsedData != null) { returnValues = parsedData; } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error parsing return variables: {ex.Message}"); } } // Clean output to remove RETURN_DATA line var cleanOutput = result.Output; if (cleanOutput.Contains("RETURN_DATA:")) { var lines = cleanOutput.Split('\n'); cleanOutput = string.Join("\n", lines.Where(line => !line.StartsWith("RETURN_DATA:"))); } return new { success = true, output = cleanOutput?.Trim() ?? "", variables = returnValues, exit_code = result.ExitCode, python_version = "CPython via PythonInterop" }; } else { return new { success = false, error = result.Error ?? "Unknown error", error_type = "PythonExecutionError", output = result.Output, exit_code = result.ExitCode }; } } finally { // Clean up temporary script file try { if (File.Exists(tempScript)) File.Delete(tempScript); } catch { } } } catch (Exception ex) { return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } } /// /// 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 = "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", "tsnet - TSNet hydraulic simulation library (if available)" }, ["common_usage_patterns"] = new[] { "len(get_objects()) - Get number of objects", "print('Hello') - Print to output", "import tsnet - Access TSNet library", "# Note: Direct object access limited in CPython mode" }, ["python_environment"] = new { type = "CPython (shared with TSNet)", interop = "PythonInterop via process execution", note = "Uses same Python environment as TSNet simulations" } }; if (!string.IsNullOrEmpty(objectName)) { // For CPython mode, provide general help about the object type switch (objectName.ToLower()) { case "app": helpInfo["app_help"] = new { description = "MainViewModel provides access to simulation state and controls", common_properties = new[] { "IsSimulationRunning", "ObjetosSimulables" }, note = "Direct access limited in CPython mode - use specific MCP tools instead" }; break; case "canvas": helpInfo["canvas_help"] = new { description = "Main drawing canvas for simulation objects", common_properties = new[] { "Width", "Height", "Children" }, note = "Direct access limited in CPython mode - use screenshot tools instead" }; break; case "objects": helpInfo["objects_help"] = new { description = "Collection of simulation objects", usage = "Use get_objects() function for safe access", note = "Direct object manipulation should use MCP tools" }; break; } } 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 { // CPython cleanup is handled by PythonInterop // No specific cleanup needed here as we use the shared TSNet environment Debug.WriteLine("[MCP Server] Python resources cleaned up (shared CPython environment)"); } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error disposing Python resources: {ex.Message}"); } } #endregion #region Debug Log Management /// /// Añade una entrada al log de debug circular /// private void AddDebugLogEntry(string message, string level = "Info") { try { var entry = new DebugLogEntry(message, level); _debugLogBuffer.Enqueue(entry); // Incrementar contador thread-safe Interlocked.Increment(ref _currentLogCount); // También escribir a Debug.WriteLine para salida inmediata Debug.WriteLine(entry.ToString()); // Forzar limpieza si el buffer está muy lleno if (_currentLogCount > MAX_LOG_ENTRIES * 1.2) { CleanupLogBuffer(null, null); } } catch (Exception ex) { // Failsafe: no usar AddDebugLogEntry aquí para evitar recursión Debug.WriteLine($"[MCP Server] Error adding debug log entry: {ex.Message}"); } } #endregion } }