CtrEditor/Services/MCPServer.cs

1694 lines
65 KiB
C#

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;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
namespace CtrEditor.Services
{
/// <summary>
/// Servidor MCP TCP para CtrEditor que permite control remoto de la aplicación
/// para debugging y testing usando el protocolo Model Context Protocol
/// </summary>
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;
// Python execution support
private ScriptEngine _pythonEngine;
private ScriptScope _pythonScope;
private readonly object _pythonLock = new object();
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 Python environment
InitializePythonEnvironment();
}
/// <summary>
/// Inicia el servidor MCP TCP
/// </summary>
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;
}
}
/// <summary>
/// Detiene el servidor MCP TCP
/// </summary>
public void Stop()
{
if (!_isRunning) return;
lock (_lockObject)
{
if (!_isRunning) return;
_isRunning = false;
_cancellationTokenSource?.Cancel();
_tcpListener?.Stop();
Debug.WriteLine("[MCP Server] Servidor detenido");
}
}
/// <summary>
/// Acepta conexiones TCP entrantes
/// </summary>
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}");
}
}
}
}
/// <summary>
/// Maneja un cliente TCP individual
/// </summary>
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");
}
}
/// <summary>
/// Envía las capabilities iniciales del servidor MCP
/// </summary>
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");
}
/// <summary>
/// Procesa una solicitud JSON-RPC
/// </summary>
private async Task<string> ProcessRequestAsync(string requestJson)
{
try
{
var request = JsonConvert.DeserializeObject<JObject>(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);
}
}
/// <summary>
/// Maneja la solicitud initialize
/// </summary>
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);
}
/// <summary>
/// Maneja la solicitud tools/list
/// </summary>
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 { }
}
},
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')"
}
}
}
}
};
return CreateSuccessResponse(id, new { tools });
}
/// <summary>
/// Crea una respuesta de éxito
/// </summary>
private string CreateSuccessResponse(JToken id, object result)
{
var response = new
{
jsonrpc = "2.0",
id = id,
result = result
};
return JsonConvert.SerializeObject(response);
}
/// <summary>
/// Maneja llamadas a herramientas (tools/call)
/// </summary>
private async Task<string> 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);
}
}
/// <summary>
/// Ejecuta una herramienta específica (debe ejecutarse en UI thread)
/// </summary>
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(),
"execute_python" => ExecutePython(arguments),
"python_help" => GetPythonHelp(arguments),
_ => throw new ArgumentException($"Unknown tool: {toolName}")
};
}
/// <summary>
/// Updates simulation timing based on current status
/// </summary>
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;
}
}
/// <summary>
/// Gets current simulation elapsed time in milliseconds
/// </summary>
private long GetCurrentSimulationMilliseconds()
{
UpdateSimulationTiming();
var currentElapsed = _simulationStopwatch.IsRunning ? _simulationStopwatch.ElapsedMilliseconds : 0;
return _totalSimulationMilliseconds + currentElapsed;
}
/// <summary>
/// Resets simulation timing counters
/// </summary>
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
/// <summary>
/// Lista todos los objetos en la simulación con metadata
/// </summary>
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 };
}
}
/// <summary>
/// Obtiene las propiedades serializables de un objeto
/// </summary>
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" };
}
}
/// <summary>
/// Crea un nuevo objeto del tipo especificado
/// </summary>
private object CreateObject(JObject arguments)
{
try
{
var typeName = arguments["type"]?.ToString();
var x = arguments["x"]?.ToObject<float>() ?? 0f;
var y = arguments["y"]?.ToObject<float>() ?? 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 };
}
}
/// <summary>
/// Actualiza las propiedades de un objeto existente
/// </summary>
private object UpdateObject(JObject arguments)
{
try
{
var objectId = arguments["id"]?.ToObject<int>();
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 };
}
}
/// <summary>
/// Aplica propiedades JSON a un objeto
/// </summary>
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}");
}
}
}
/// <summary>
/// Elimina objetos por ID
/// </summary>
private object DeleteObjects(JObject arguments)
{
try
{
var ids = new List<int>();
// Soportar tanto ID único como array de IDs
if (arguments["id"] != null)
{
ids.Add(arguments["id"].ToObject<int>());
}
else if (arguments["ids"] != null)
{
ids.AddRange(arguments["ids"].ToObject<int[]>());
}
else
{
throw new ArgumentException("Either 'id' or 'ids' is required");
}
var deletedCount = 0;
var errors = new List<string>();
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 };
}
}
/// <summary>
/// Lista todos los tipos de objetos disponibles para crear
/// </summary>
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 };
}
}
/// <summary>
/// Inicia la simulación
/// </summary>
private object StartSimulation(JObject arguments)
{
try
{
var duration = arguments["duration"]?.ToObject<double?>(); // 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 };
}
}
/// <summary>
/// Detiene la simulación
/// </summary>
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 };
}
}
/// <summary>
/// Obtiene el estado actual de la simulación
/// </summary>
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 };
}
}
/// <summary>
/// Obtiene el estado de conexión del PLC
/// </summary>
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 };
}
}
/// <summary>
/// Toma una captura de pantalla del canvas
/// </summary>
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<bool>() ?? false; // Por defecto false
var x = arguments["x"]?.ToObject<float?>();
var y = arguments["y"]?.ToObject<float?>();
var width = arguments["width"]?.ToObject<float?>();
var height = arguments["height"]?.ToObject<float?>();
// 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
};
}
}
/// <summary>
/// Guarda el proyecto actual
/// </summary>
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
/// <summary>
/// Toma una captura de pantalla del canvas
/// </summary>
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 Python Execution Support
/// <summary>
/// Initializes the Python environment with enhanced libraries and thread-safe print function
/// </summary>
private void InitializePythonEnvironment()
{
try
{
// Set console output encoding to avoid codepage issues
try
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
}
catch
{
// Ignore encoding setup errors
}
_pythonEngine = Python.CreateEngine();
_pythonScope = _pythonEngine.CreateScope();
// Set up enhanced search paths for IronPython.StdLib
var searchPaths = _pythonEngine.GetSearchPaths();
// Add current directory and common library paths
var currentDir = Directory.GetCurrentDirectory();
searchPaths.Add(currentDir);
searchPaths.Add(System.IO.Path.Combine(currentDir, "lib"));
searchPaths.Add(System.IO.Path.Combine(currentDir, "Lib"));
_pythonEngine.SetSearchPaths(searchPaths);
// Import basic libraries and set up global variables
var setupScript = @"
import sys
# Fix encoding issues before importing anything else
try:
import codecs
# Override the problematic codepage lookup
def search_function(encoding):
if 'codepage' in encoding.lower():
return codecs.lookup('utf-8')
return None
codecs.register(search_function)
except:
pass
import clr
import math
import time
import json
import random
# Add .NET types
clr.AddReference('System')
clr.AddReference('System.Core')
clr.AddReference('PresentationCore')
clr.AddReference('PresentationFramework')
from System import Console, Text
from System.Text import StringBuilder
# Create completely isolated print system to avoid encoding issues
_print_buffer = StringBuilder()
def safe_print(*args, **kwargs):
'''Completely isolated print function that avoids all encoding issues'''
try:
separator = kwargs.get('sep', ' ')
end_char = kwargs.get('end', '\n')
# Convert all arguments to strings safely
text_parts = []
for arg in args:
try:
text_parts.append(str(arg))
except:
text_parts.append('<unprintable>')
text = separator.join(text_parts) + end_char
# Store in our isolated buffer
_print_buffer.Append(text)
# Also write to debug output for monitoring
try:
import System.Diagnostics
System.Diagnostics.Debug.WriteLine('[Python] ' + text.rstrip())
except:
pass
except Exception as e:
try:
_print_buffer.Append(f'Print error: {e}\n')
except:
pass
# Completely replace print function - no fallback to original
print = safe_print
# Helper function to get print output
def get_print_output():
'''Get accumulated print output and clear buffer'''
try:
output = _print_buffer.ToString()
_print_buffer.Clear()
return output
except:
return ''
def get_objects():
'''Helper function to get all simulable objects as a list'''
try:
return list(objects) if objects else []
except:
return []
";
_pythonEngine.Execute(setupScript, _pythonScope);
Debug.WriteLine("[MCP Server] Python environment initialized successfully");
}
catch (Exception ex)
{
Debug.WriteLine($"[MCP Server] Error initializing Python environment: {ex.Message}");
Debug.WriteLine($"[MCP Server] Stack trace: {ex.StackTrace}");
}
}
/// <summary>
/// Executes Python code with access to CtrEditor objects
/// </summary>
private object ExecutePython(JObject arguments)
{
lock (_pythonLock)
{
try
{
var code = arguments["code"]?.ToString();
if (string.IsNullOrEmpty(code))
throw new ArgumentException("Code is required");
var returnVariables = arguments["return_variables"]?.ToObject<string[]>() ?? new string[0];
var timeoutSeconds = arguments["timeout_seconds"]?.ToObject<int>() ?? 30;
// Set up context variables with thread-safe access
return Application.Current.Dispatcher.Invoke<object>(() =>
{
try
{
// Set global variables for Python script
_pythonScope.SetVariable("app", _mainViewModel);
_pythonScope.SetVariable("canvas", _mainViewModel.MainCanvas);
_pythonScope.SetVariable("objects", _mainViewModel.ObjetosSimulables);
// Execute the Python code directly on UI thread to avoid cross-thread issues
// Note: This runs synchronously on UI thread but IronPython is generally fast
_pythonEngine.Execute(code, _pythonScope);
// Get print output
var printOutput = "";
try
{
var getPrintOutput = _pythonScope.GetVariable("get_print_output");
if (getPrintOutput != null)
{
var result = _pythonEngine.Operations.Invoke(getPrintOutput);
printOutput = result?.ToString() ?? "";
}
}
catch (Exception ex)
{
Debug.WriteLine($"[MCP Server] Error getting print output: {ex.Message}");
}
// Collect return variables
var returnValues = new Dictionary<string, object>();
foreach (var varName in returnVariables)
{
try
{
if (_pythonScope.ContainsVariable(varName))
{
var value = _pythonScope.GetVariable(varName);
returnValues[varName] = ConvertPythonObject(value);
}
else
{
returnValues[varName] = null;
}
}
catch (Exception ex)
{
returnValues[varName] = $"Error getting variable: {ex.Message}";
}
}
return new
{
success = true,
output = printOutput,
variables = returnValues,
execution_time_ms = "< 1000"
};
}
catch (Exception ex)
{
Debug.WriteLine($"[MCP Server] Python execution error: {ex.Message}");
return new
{
success = false,
error = ex.Message,
error_type = ex.GetType().Name
};
}
});
}
catch (Exception ex)
{
return new
{
success = false,
error = ex.Message,
error_type = ex.GetType().Name
};
}
}
}
/// <summary>
/// Converts Python objects to JSON-serializable .NET objects with better type handling
/// </summary>
private object ConvertPythonObject(dynamic pythonObj)
{
if (pythonObj == null) return null;
try
{
// Handle basic .NET types
if (pythonObj is string || pythonObj is int || pythonObj is double ||
pythonObj is bool || pythonObj is decimal)
{
return pythonObj;
}
// Handle System.Single (float) conversion
if (pythonObj is float || pythonObj.GetType() == typeof(System.Single))
{
return Convert.ToDouble(pythonObj);
}
// Handle nullable types
if (pythonObj.GetType().IsGenericType &&
pythonObj.GetType().GetGenericTypeDefinition() == typeof(Nullable<>))
{
var underlyingValue = pythonObj.HasValue ? pythonObj.Value : null;
return underlyingValue != null ? ConvertPythonObject(underlyingValue) : null;
}
// Handle collections
if (pythonObj is System.Collections.IEnumerable enumerable && !(pythonObj is string))
{
var list = new List<object>();
foreach (var item in enumerable)
{
list.Add(ConvertPythonObject(item));
}
return list;
}
// Handle objects with simple properties
var type = pythonObj.GetType();
if (type.IsClass && !type.FullName.StartsWith("System."))
{
var properties = new Dictionary<string, object>();
var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var validProps = new List<PropertyInfo>();
// Filter properties manually to avoid lambda issues
foreach (var prop in allProps)
{
if (prop.CanRead && prop.GetIndexParameters().Length == 0)
{
validProps.Add(prop);
if (validProps.Count >= 20) // Limit to prevent infinite recursion
break;
}
}
foreach (var prop in validProps)
{
try
{
var value = prop.GetValue(pythonObj);
properties[prop.Name] = ConvertPythonObject(value);
}
catch
{
properties[prop.Name] = $"<Error reading {prop.Name}>";
}
}
return properties;
}
// Fallback: convert to string
return pythonObj.ToString();
}
catch (Exception ex)
{
return $"<Conversion error: {ex.Message}>";
}
}
/// <summary>
/// Gets help information about available Python objects and methods
/// </summary>
private object GetPythonHelp(JObject arguments)
{
try
{
var objectName = arguments["object_name"]?.ToString();
var helpInfo = new Dictionary<string, object>
{
["available_objects"] = new
{
app = "MainViewModel - Main application view model with all CtrEditor functionality",
canvas = "Canvas - Main canvas where objects are displayed",
objects = "ObservableCollection<osBase> - Collection of all simulable objects",
get_objects = "Function() - Helper function that returns objects as a Python list"
},
["available_libraries"] = new[]
{
"sys - System-specific parameters and functions",
"math - Mathematical functions",
"time - Time-related functions",
"json - JSON encoder and decoder",
"random - Random number generation",
"clr - .NET CLR integration"
},
["common_usage_patterns"] = new[]
{
"len(objects) - Get number of objects",
"objects[0].Id.Value - Get ID of first object",
"app.IsSimulationRunning - Check if simulation is running",
"canvas.Width, canvas.Height - Get canvas dimensions",
"print('Hello') - Print to output (thread-safe)"
}
};
if (!string.IsNullOrEmpty(objectName))
{
Application.Current.Dispatcher.Invoke(() =>
{
try
{
object targetObject = objectName.ToLower() switch
{
"app" => _mainViewModel,
"canvas" => _mainViewModel.MainCanvas,
"objects" => _mainViewModel.ObjetosSimulables,
_ => null
};
if (targetObject != null)
{
var type = targetObject.GetType();
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m => !m.IsSpecialName && m.DeclaringType != typeof(object))
.Take(20)
.Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"))})")
.ToArray();
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead)
.Take(20)
.Select(p => $"{p.Name} : {p.PropertyType.Name}")
.ToArray();
helpInfo[$"{objectName}_methods"] = methods;
helpInfo[$"{objectName}_properties"] = properties;
}
}
catch (Exception ex)
{
helpInfo["error"] = $"Error getting help for {objectName}: {ex.Message}";
}
});
}
return new
{
success = true,
help = helpInfo
};
}
catch (Exception ex)
{
return new
{
success = false,
error = ex.Message
};
}
}
#endregion
#region Helper Methods
/// <summary>
/// Crea una respuesta de error JSON-RPC
/// </summary>
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 Python resources
try
{
// ScriptScope doesn't have Dispose, just clear variables
_pythonScope?.RemoveVariable("app");
_pythonScope?.RemoveVariable("canvas");
_pythonScope?.RemoveVariable("objects");
_pythonEngine?.Runtime?.Shutdown();
}
catch (Exception ex)
{
Debug.WriteLine($"[MCP Server] Error disposing Python resources: {ex.Message}");
}
}
#endregion
}
}