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 CtrEditor.HydraulicSimulator; 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(); // 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)); } /// /// 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 = "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 = "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 "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(); case "execute_tsnet_direct": return ExecuteTSNetDirect(arguments); 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 }; } } /// /// 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 }; } } #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"); // Serialize objects information for Python context var objectsInfo = new List(); var appInfo = new Dictionary(); var canvasInfo = new Dictionary(); await Application.Current.Dispatcher.InvokeAsync(() => { try { // Serialize basic object information if (_mainViewModel?.ObjetosSimulables != null) { foreach (var obj in _mainViewModel.ObjetosSimulables) { objectsInfo.Add(new { type = obj.GetType().Name, nombre = obj.Nombre ?? "Sin nombre", id = obj.Id?.ToString() ?? "Sin ID", left = obj.Left, top = obj.Top, ancho = obj.Ancho, alto = obj.Alto, is_hydraulic = obj is IHydraulicComponent, has_hydraulic_components = (obj is IHydraulicComponent hc) ? hc.HasHydraulicComponents : false }); } } // App information appInfo["total_objects"] = objectsInfo.Count; appInfo["is_simulation_running"] = _mainViewModel?.IsSimulationRunning ?? false; // Canvas information if (_mainViewModel?.MainCanvas != null) { canvasInfo["width_pixels"] = _mainViewModel.MainCanvas.ActualWidth; canvasInfo["height_pixels"] = _mainViewModel.MainCanvas.ActualHeight; } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error serializing objects: {ex.Message}"); } }); var objectsJson = JsonConvert.SerializeObject(objectsInfo, Formatting.None); var appInfoJson = JsonConvert.SerializeObject(appInfo, Formatting.None); var canvasInfoJson = JsonConvert.SerializeObject(canvasInfo, Formatting.None); // Prepare enhanced script with global variables and helpers var enhancedScript = $@" # Set up CtrEditor context variables import sys import json import math import time # Deserialize CtrEditor objects and context _objects_data = json.loads('''{objectsJson}''') _app_data = json.loads('''{appInfoJson}''') _canvas_data = json.loads('''{canvasInfoJson}''') # Mock objects for compatibility class MockObject: def __init__(self, data): for key, value in data.items(): setattr(self, key.replace('-', '_'), value) def GetType(self): class MockType: def __init__(self, name): self.Name = name return MockType(self.type) class MockApp: def __init__(self, data): for key, value in data.items(): setattr(self, key, value) class MockCanvas: def __init__(self, data): for key, value in data.items(): setattr(self, key, value) # Create global context variables objects = [MockObject(obj_data) for obj_data in _objects_data] app = MockApp(_app_data) canvas = MockCanvas(_canvas_data) # Helper functions def get_objects(): return objects def safe_print(*args, **kwargs): try: print(*args, **kwargs) import sys sys.stdout.flush() except Exception as e: import sys sys.stderr.write(f""Error in safe_print: {{e}}\n"") sys.stderr.flush() # 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)) "; // Use enhanced Python context with real object access var result = await ExecutePythonWithRealObjects(code, returnVariables, timeoutSeconds); return result; } catch (Exception ex) { return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } } /// /// Executes Python with real CtrEditor objects injected /// private async Task ExecutePythonWithRealObjects(string pythonScript, string[] returnVariables, int timeoutSeconds) { try { // TODO: Implement real object injection using embedded CPython // For now, create enhanced proxies that delegate to real objects var realObjectsScript = await CreateRealObjectsContext(); var fullScript = realObjectsScript + "\n\n" + pythonScript; // Write to temporary file for execution var tempScript = System.IO.Path.GetTempFileName().Replace(".tmp", ".py"); await File.WriteAllTextAsync(tempScript, fullScript); try { var result = await PythonInterop.ExecuteScriptAsync(tempScript); return new { success = result.Success, output = result.Output ?? "", error = result.Error, exit_code = result.ExitCode, variables = new Dictionary(), // TODO: Implement variable extraction python_version = "CPython via PythonInterop" }; } finally { try { File.Delete(tempScript); } catch { } } } catch (Exception ex) { return new { success = false, error = ex.Message, error_type = ex.GetType().Name }; } } /// /// Creates Python context with real object access /// private async Task CreateRealObjectsContext() { var objectsData = new List>(); await Application.Current.Dispatcher.InvokeAsync(() => { try { if (_mainViewModel?.ObjetosSimulables != null) { foreach (var obj in _mainViewModel.ObjetosSimulables) { var objData = new Dictionary { ["type"] = obj.GetType().Name, ["nombre"] = obj.Nombre ?? "Sin nombre", ["id"] = obj.Id?.ToString() ?? "Sin ID" }; // Add hydraulic-specific properties for real access if (obj.GetType().Name.Contains("osHydTank")) { var tank = obj as dynamic; objData["CurrentLevel"] = tank?.CurrentLevel ?? 0.0; objData["MaxLevel"] = tank?.MaxLevel ?? 0.0; objData["Diameter"] = tank?.Diameter ?? 0.0; objData["IsFixedPressure"] = tank?.IsFixedPressure ?? false; } else if (obj.GetType().Name.Contains("osHydPump")) { var pump = obj as dynamic; objData["CurrentFlow"] = pump?.CurrentFlow ?? 0.0; objData["MaxFlow"] = pump?.MaxFlow ?? 0.0; objData["IsRunning"] = pump?.IsRunning ?? false; } else if (obj.GetType().Name.Contains("osHydPipe")) { var pipe = obj as dynamic; objData["CurrentFlow"] = pipe?.CurrentFlow ?? 0.0; objData["Diameter"] = pipe?.Diameter ?? 0.0; objData["Length"] = pipe?.Length ?? 0.0; } objectsData.Add(objData); } } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error creating real objects context: {ex.Message}"); } }); var objectsJson = JsonConvert.SerializeObject(objectsData, Formatting.None); return $@" # CtrEditor Real Objects Context import json # Real object data from C# _real_objects_data = json.loads('''{objectsJson}''') # Enhanced object proxies with real data class RealObjectProxy: def __init__(self, data): self._data = data # Set all properties as attributes for key, value in data.items(): setattr(self, key.replace('-', '_'), value) @property def Nombre(self): return self._data.get('nombre', 'Unknown') def GetType(self): class RealType: def __init__(self, name): self.Name = name return RealType(self._data.get('type', 'Unknown')) # Create real object proxies objects = [RealObjectProxy(obj_data) for obj_data in _real_objects_data] # App proxy with real methods class RealAppProxy: def __init__(self): self.total_objects = len(objects) self.tsnetSimulationManager = self # Self-reference for compatibility def RunTSNetSimulationSync(self): print(""RealAppProxy: RunTSNetSimulationSync called (proxy - would call real C# method)"") return True app = RealAppProxy() # Canvas proxy class RealCanvasProxy: def __init__(self): self.width = 1024 self.height = 768 canvas = RealCanvasProxy() print(f""Real CtrEditor context initialized with {{len(objects)}} objects"") "; } /// /// 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 specific MCP 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}"); } } /// /// Ejecuta TSNet directamente sin usar Python /// private object ExecuteTSNetDirect(JObject arguments) { try { AddDebugLogEntry("[TSNet Direct] Iniciando ejecución directa de TSNet"); // Verificar que el MainViewModel existe if (_mainViewModel == null) { return new { success = false, error = "MainViewModel no disponible" }; } // Ejecutar el método RunTSNetSimulationSync directamente _mainViewModel.RunTSNetSimulationSync(); AddDebugLogEntry("[TSNet Direct] TSNet ejecutado exitosamente"); return new { success = true, message = "TSNet ejecutado directamente desde C# sin Python", method = "RunTSNetSimulationSync" }; } catch (Exception ex) { AddDebugLogEntry($"[TSNet Direct] Error: {ex.Message}", "Error"); return new { success = false, error = ex.Message, stackTrace = ex.StackTrace }; } } #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) { // Limpiar buffer directamente sin pasar por el evento lock (_debugLogBuffer) { while (_debugLogBuffer.Count > MAX_LOG_ENTRIES / 2) { _debugLogBuffer.TryDequeue(out _); } _currentLogCount = _debugLogBuffer.Count; } } } catch (Exception ex) { // Failsafe: no usar AddDebugLogEntry aquí para evitar recursión Debug.WriteLine($"[MCP Server] Error adding debug log entry: {ex.Message}"); } } #endregion } }