using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; 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; namespace CtrEditor.Services { /// /// 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; 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; } /// /// 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 = "save_project", description = "Save the current project" }, new { name = "reset_simulation_timing", description = "Reset simulation timing counters" } } }, 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)" } } } }, 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 { } } } }; 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), "save_project" => SaveProject(), "reset_simulation_timing" => ResetSimulationTiming(), _ => 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 /// private object TakeScreenshot(JObject arguments) { try { // Parámetros de screenshot var filename = arguments["filename"]?.ToString() ?? $"screenshot_{DateTime.Now:yyyyMMdd_HHmmss}.png"; var includeBackground = arguments["include_background"]?.ToObject() ?? false; // Por defecto false var x = arguments["x"]?.ToObject(); var y = arguments["y"]?.ToObject(); var width = arguments["width"]?.ToObject(); var height = arguments["height"]?.ToObject(); // Asegurar extensión .png if (!filename.ToLower().EndsWith(".png")) filename += ".png"; // Crear subdirectorio screenshots var screenshotsDir = System.IO.Path.Combine(EstadoPersistente.Instance.directorio, "screenshots"); Directory.CreateDirectory(screenshotsDir); // Obtener ruta completa - siempre en subdirectorio screenshots a menos que sea ruta absoluta var fullPath = System.IO.Path.IsPathRooted(filename) ? filename : System.IO.Path.Combine(screenshotsDir, filename); // Obtener información del canvas para detalles var canvas = _mainViewModel.MainCanvas; var canvasWidth = canvas?.ActualWidth ?? 0; var canvasHeight = canvas?.ActualHeight ?? 0; var canvasWidthMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasWidth); var canvasHeightMeters = PixelToMeter.Instance.calc.PixelsToMeters((float)canvasHeight); // Tomar screenshot var success = TakeCanvasScreenshot(fullPath, includeBackground, x, y, width, height); if (success) { var fileInfo = new FileInfo(fullPath); return new { success = true, filename = System.IO.Path.GetFileName(fullPath), full_path = fullPath, directory = System.IO.Path.GetDirectoryName(fullPath), file_size_bytes = fileInfo.Length, timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), message = "Screenshot saved successfully", canvas_info = new { canvas_width_pixels = canvasWidth, canvas_height_pixels = canvasHeight, canvas_width_meters = Math.Round(canvasWidthMeters, 3), canvas_height_meters = Math.Round(canvasHeightMeters, 3) }, capture_info = new { include_background = includeBackground, area_specified = x.HasValue && y.HasValue && width.HasValue && height.HasValue, area = x.HasValue ? new { x = Math.Round(x.Value, 3), y = Math.Round(y.Value, 3), width = Math.Round(width.Value, 3), height = Math.Round(height.Value, 3), units = "meters" } : null, capture_type = x.HasValue ? "partial" : "full_canvas" } }; } else { return new { success = false, error = "Failed to capture screenshot", attempted_path = fullPath }; } } 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 }; } } #endregion #region Screenshot Implementation /// /// Toma una captura de pantalla del canvas /// private bool TakeCanvasScreenshot(string filePath, bool includeBackground = false, float? x = null, float? y = null, float? width = null, float? height = null) { try { var canvas = _mainViewModel.MainCanvas; if (canvas == null) { Debug.WriteLine("[MCP Server] Canvas is null"); return false; } // Asegurar que el canvas esté renderizado canvas.UpdateLayout(); // Determinar el área a capturar Rect captureRect; if (x.HasValue && y.HasValue && width.HasValue && height.HasValue) { // Convertir coordenadas de metros a píxeles var pixelX = PixelToMeter.Instance.calc.MetersToPixels(x.Value); var pixelY = PixelToMeter.Instance.calc.MetersToPixels(y.Value); var pixelWidth = PixelToMeter.Instance.calc.MetersToPixels(width.Value); var pixelHeight = PixelToMeter.Instance.calc.MetersToPixels(height.Value); captureRect = new Rect(pixelX, pixelY, pixelWidth, pixelHeight); } else { // Capturar todo el canvas captureRect = new Rect(0, 0, canvas.ActualWidth, canvas.ActualHeight); } // Validar dimensiones if (captureRect.Width <= 0 || captureRect.Height <= 0) { Debug.WriteLine($"[MCP Server] Invalid capture dimensions: {captureRect}"); return false; } Debug.WriteLine($"[MCP Server] Capturing area: {captureRect}, Canvas size: {canvas.ActualWidth}x{canvas.ActualHeight}"); // Crear RenderTargetBitmap con alta resolución // Usar factor de escala para mejorar calidad en capturas parciales var scaleFactor = (x.HasValue && y.HasValue) ? 3.0 : 2.0; // Mayor escala para áreas parciales var renderWidth = Math.Max(1, (int)(captureRect.Width * scaleFactor)); var renderHeight = Math.Max(1, (int)(captureRect.Height * scaleFactor)); var dpi = 96 * scaleFactor; // Aumentar DPI proporcionalmente var renderBitmap = new RenderTargetBitmap( renderWidth, renderHeight, dpi, // DPI X dpi, // DPI Y PixelFormats.Pbgra32); // Crear un Canvas temporal para renderizado con escala mejorada var tempCanvas = new Canvas() { Width = captureRect.Width * scaleFactor, Height = captureRect.Height * scaleFactor, Background = includeBackground ? canvas.Background : Brushes.White }; // Aplicar escala al canvas tempCanvas.RenderTransform = new ScaleTransform(scaleFactor, scaleFactor); // Clonar elementos visibles del canvas principal foreach (UIElement child in canvas.Children) { if (child.Visibility == Visibility.Visible) { try { // Obtener posición del elemento var left = Canvas.GetLeft(child); var top = Canvas.GetTop(child); // Verificar si está en el área de captura var elementRect = new Rect( double.IsNaN(left) ? 0 : left, double.IsNaN(top) ? 0 : top, child.RenderSize.Width, child.RenderSize.Height); if (captureRect.IntersectsWith(elementRect) || (!x.HasValue && !y.HasValue)) { // Crear una representación visual del elemento var visualBrush = new VisualBrush(child) { Stretch = Stretch.None, AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top }; var rect = new Rectangle() { Width = child.RenderSize.Width * scaleFactor, Height = child.RenderSize.Height * scaleFactor, Fill = visualBrush }; // Posicionar relativo al área de captura con escala Canvas.SetLeft(rect, (elementRect.X - captureRect.X) * scaleFactor); Canvas.SetTop(rect, (elementRect.Y - captureRect.Y) * scaleFactor); tempCanvas.Children.Add(rect); } } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error processing child element: {ex.Message}"); } } } // Forzar layout del canvas temporal con las nuevas dimensiones escaladas var scaledSize = new Size(captureRect.Width * scaleFactor, captureRect.Height * scaleFactor); tempCanvas.Measure(scaledSize); tempCanvas.Arrange(new Rect(0, 0, scaledSize.Width, scaledSize.Height)); tempCanvas.UpdateLayout(); // Renderizar renderBitmap.Render(tempCanvas); // Guardar imagen var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(renderBitmap)); Directory.CreateDirectory(System.IO.Path.GetDirectoryName(filePath)); using (var fileStream = new FileStream(filePath, FileMode.Create)) { encoder.Save(fileStream); } Debug.WriteLine($"[MCP Server] Screenshot saved successfully: {filePath}"); return true; } catch (Exception ex) { Debug.WriteLine($"[MCP Server] Error taking screenshot: {ex.Message}"); Debug.WriteLine($"[MCP Server] Stack trace: {ex.StackTrace}"); return false; } } #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(); } #endregion } }