1928 lines
72 KiB
C#
1928 lines
72 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Representa una entrada de log con timestamp
|
|
/// </summary>
|
|
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}";
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
|
|
// Circular debug log system
|
|
private readonly ConcurrentQueue<DebugLogEntry> _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<DebugLogEntry>();
|
|
_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));
|
|
}
|
|
|
|
/// <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}");
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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");
|
|
AddDebugLogEntry("[MCP Server] Servidor detenido");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifica si el dispatcher está disponible sin bloquear
|
|
/// </summary>
|
|
private bool IsDispatcherAvailable()
|
|
{
|
|
try
|
|
{
|
|
return Application.Current != null && Application.Current.Dispatcher != null && !Application.Current.Dispatcher.HasShutdownStarted;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ejecuta una acción en el dispatcher de forma segura con timeout
|
|
/// </summary>
|
|
private async Task<T> SafeDispatcherInvokeAsync<T>(Func<T> 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");
|
|
}
|
|
}
|
|
|
|
/// <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 = "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");
|
|
}
|
|
|
|
/// <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 = "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 });
|
|
}
|
|
|
|
/// <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}");
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ejecuta una herramienta específica (debe ejecutarse en UI thread)
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
{
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
{
|
|
if (!IsDispatcherAvailable())
|
|
{
|
|
return new { success = false, error = "CtrEditor not available" };
|
|
}
|
|
|
|
var duration = arguments["duration"]?.ToObject<double?>(); // 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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detiene la simulación
|
|
/// </summary>
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Obtiene el estado actual de la simulación
|
|
/// </summary>
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
/// 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 Python Execution Support
|
|
|
|
/// <summary>
|
|
/// Executes Python code using the shared CPython environment from TSNet
|
|
/// </summary>
|
|
private async Task<object> 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<string[]>() ?? new string[0];
|
|
var timeoutSeconds = arguments["timeout_seconds"]?.ToObject<int>() ?? 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<object>();
|
|
var appInfo = new Dictionary<string, object>();
|
|
var canvasInfo = new Dictionary<string, object>();
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes Python with real CtrEditor objects injected
|
|
/// </summary>
|
|
private async Task<object> 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<string, object>(), // 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
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates Python context with real object access
|
|
/// </summary>
|
|
private async Task<string> CreateRealObjectsContext()
|
|
{
|
|
var objectsData = new List<Dictionary<string, object>>();
|
|
|
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
|
{
|
|
try
|
|
{
|
|
if (_mainViewModel?.ObjetosSimulables != null)
|
|
{
|
|
foreach (var obj in _mainViewModel.ObjetosSimulables)
|
|
{
|
|
var objData = new Dictionary<string, object>
|
|
{
|
|
["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"")
|
|
";
|
|
}
|
|
|
|
/// <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 = "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
|
|
|
|
/// <summary>
|
|
/// Custom TraceListener for capturing debug output
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a log entry to the circular buffer
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Timer callback to cleanup old log entries
|
|
/// </summary>
|
|
private void CleanupLogBuffer(object sender, System.Timers.ElapsedEventArgs e)
|
|
{
|
|
lock (_logLock)
|
|
{
|
|
while (_currentLogCount > MAX_LOG_ENTRIES && _debugLogBuffer.TryDequeue(out _))
|
|
{
|
|
Interlocked.Decrement(ref _currentLogCount);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Search debug log with pattern matching
|
|
/// </summary>
|
|
private object SearchDebugLog(JObject arguments)
|
|
{
|
|
try
|
|
{
|
|
var pattern = arguments?["pattern"]?.ToString() ?? "";
|
|
var maxLines = arguments?["max_lines"]?.ToObject<int>() ?? 100;
|
|
var lastNLines = arguments?["last_n_lines"]?.ToObject<int>() ?? 1000;
|
|
var caseSensitive = arguments?["case_sensitive"]?.ToObject<bool>() ?? 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<DebugLogEntry>();
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get debug log statistics
|
|
/// </summary>
|
|
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
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clear the debug log buffer
|
|
/// </summary>
|
|
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
|
|
|
|
/// <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 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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ejecuta TSNet directamente sin usar Python
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Añade una entrada al log de debug circular
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|