CtrEditor/Services/MCPServer.cs

2091 lines
82 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();
private ScreenshotManager _screenshotManager;
// 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));
// ScreenshotManager se inicializará de forma lazy cuando se necesite
_screenshotManager = null;
}
/// <summary>
/// Obtiene el ScreenshotManager, inicializándolo si es necesario
/// </summary>
private ScreenshotManager GetScreenshotManager()
{
if (_screenshotManager == null)
{
var canvas = _mainViewModel?.MainCanvas;
if (canvas == null)
{
throw new InvalidOperationException("Canvas no está disponible. Asegúrate de que la UI esté completamente cargada.");
}
_screenshotManager = new ScreenshotManager(_mainViewModel, canvas);
}
return _screenshotManager;
}
/// <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 = "take_screenshot", description = "Take a screenshot of the canvas" },
new { name = "take_object_screenshot", description = "Take a screenshot of specific objects by their IDs" },
new { name = "save_project", description = "Save the current project" },
new { name = "reset_simulation_timing", description = "Reset simulation timing counters" },
new { name = "search_debug_log", description = "Search debug log entries with pattern matching" },
new { name = "get_debug_stats", description = "Get debug log buffer statistics" },
new { name = "clear_debug_buffer", description = "Clear the debug log buffer" },
new { name = "execute_python", description = "Execute Python code with access to CtrEditor objects" },
new { name = "python_help", description = "Get help about available Python objects and methods" }
}
},
serverInfo = new
{
name = "CtrEditor MCP Server",
version = "1.0.0",
description = "CtrEditor WPF Application MCP Server for debugging and testing"
}
}
};
var json = JsonConvert.SerializeObject(capabilities);
await writer.WriteLineAsync(json);
Debug.WriteLine("[MCP Server] Capabilities enviadas");
}
/// <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)"
},
center_coordinates = new {
type = "boolean",
description = "If true, x and y parameters represent the center of the capture area instead of top-left corner. Defaults to false for backward compatibility."
}
}
}
},
new {
name = "take_object_screenshot",
description = "Take a screenshot of specific objects by their IDs. This automatically calculates the bounding box of the specified objects and captures only that area with optional padding.",
inputSchema = new {
type = "object",
properties = new {
object_ids = new {
type = "array",
items = new { type = "string" },
description = "Array of object IDs to capture. Can be a single object or multiple objects."
},
padding_meters = new {
type = "number",
description = "Additional padding around objects in meters. Defaults to 0.5 meters."
},
filename = new {
type = "string",
description = "Optional filename for the screenshot. Defaults to timestamp-based name."
},
include_background = new {
type = "boolean",
description = "Whether to include canvas background image. Defaults to false."
}
},
required = new[] { "object_ids" }
}
},
new {
name = "start_simulation",
description = "Start the physics simulation",
inputSchema = new {
type = "object",
properties = new { }
}
},
new {
name = "stop_simulation",
description = "Stop the physics simulation",
inputSchema = new {
type = "object",
properties = new { }
}
},
new {
name = "reset_simulation_timing",
description = "Reset simulation timing counters to zero",
inputSchema = new {
type = "object",
properties = new { }
}
},
new {
name = "execute_python",
description = "Execute Python code with access to CtrEditor objects and canvas for debugging",
inputSchema = new {
type = "object",
properties = new {
code = new {
type = "string",
description = "Python code to execute. Has access to 'app' (MainViewModel), 'canvas', 'objects' (simulable objects), and common libraries."
},
return_variables = new {
type = "array",
items = new { type = "string" },
description = "Optional list of variable names to return from Python execution"
},
timeout_seconds = new {
type = "number",
description = "Execution timeout in seconds. Defaults to 30."
}
},
required = new[] { "code" }
}
},
new {
name = "python_help",
description = "Get help about available Python objects and methods in CtrEditor context",
inputSchema = new {
type = "object",
properties = new {
object_name = new {
type = "string",
description = "Optional specific object to get help for (e.g., 'app', 'canvas', 'objects')"
}
}
}
},
new {
name = "search_debug_log",
description = "Search debug log entries with pattern matching (regex or text)",
inputSchema = new {
type = "object",
properties = new {
pattern = new {
type = "string",
description = "Search pattern (regex or plain text). Empty returns recent entries."
},
max_lines = new {
type = "number",
description = "Maximum number of matching lines to return. Defaults to 100."
},
last_n_lines = new {
type = "number",
description = "Search only in the last N log entries. Defaults to 1000."
},
case_sensitive = new {
type = "boolean",
description = "Whether search should be case sensitive. Defaults to false."
}
}
}
},
new {
name = "get_debug_stats",
description = "Get debug log buffer statistics and status",
inputSchema = new {
type = "object",
properties = new { }
}
},
new {
name = "clear_debug_buffer",
description = "Clear the debug log buffer",
inputSchema = new {
type = "object",
properties = new { }
}
}
};
return CreateSuccessResponse(id, new { tools });
}
/// <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 "take_screenshot":
return TakeScreenshot(arguments);
case "take_object_screenshot":
return TakeObjectScreenshot(arguments);
case "save_project":
return SaveProject();
case "reset_simulation_timing":
return ResetSimulationTiming();
case "python_help":
return GetPythonHelp(arguments);
case "search_debug_log":
return SearchDebugLog(arguments);
case "get_debug_stats":
return GetDebugStats();
case "clear_debug_buffer":
return ClearDebugBuffer();
default:
throw new ArgumentException($"Unknown tool: {toolName}");
}
}
/// <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>
/// Toma una captura de pantalla del canvas usando el nuevo ScreenshotManager
/// </summary>
private object TakeScreenshot(JObject arguments)
{
try
{
var filename = arguments["filename"]?.ToString();
var includeBackground = arguments["include_background"]?.ToObject<bool>() ?? false;
var saveFile = arguments["save_file"]?.ToObject<bool>() ?? true;
var returnBase64 = arguments["return_base64"]?.ToObject<bool>() ?? true;
var x = arguments["x"]?.ToObject<float?>();
var y = arguments["y"]?.ToObject<float?>();
var width = arguments["width"]?.ToObject<float?>();
var height = arguments["height"]?.ToObject<float?>();
var centerCoordinates = arguments["center_coordinates"]?.ToObject<bool>() ?? false;
// Obtener ScreenshotManager de forma lazy
var screenshotManager = GetScreenshotManager();
ScreenshotResult result;
if (x.HasValue && y.HasValue && width.HasValue && height.HasValue)
{
// Captura de área específica
if (centerCoordinates)
{
result = screenshotManager.CaptureCenteredArea(
x.Value, y.Value, width.Value, height.Value,
filename, includeBackground, saveFile, returnBase64);
}
else
{
result = screenshotManager.CaptureArea(
x.Value, y.Value, width.Value, height.Value,
filename, includeBackground, saveFile, returnBase64);
}
}
else
{
// Captura de todo el canvas
result = screenshotManager.CaptureFullCanvas(
filename, includeBackground, saveFile, returnBase64);
}
if (result.Success)
{
return new
{
success = true,
filename = result.FileName,
full_path = result.FilePath,
directory = result.Directory,
file_size_bytes = result.FileSizeBytes,
timestamp = result.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
message = "Screenshot saved successfully",
base64_data = returnBase64 ? result.Base64Data : null,
canvas_info = new
{
canvas_width_pixels = _mainViewModel.MainCanvas?.ActualWidth ?? 0,
canvas_height_pixels = _mainViewModel.MainCanvas?.ActualHeight ?? 0,
canvas_width_meters = result.CaptureArea?.Width ?? 0,
canvas_height_meters = result.CaptureArea?.Height ?? 0
},
capture_info = new
{
include_background = result.IncludeBackground,
area_specified = x.HasValue && y.HasValue && width.HasValue && height.HasValue,
area = result.CaptureArea != null ? new
{
x = Math.Round(result.CaptureArea.Left, 3),
y = Math.Round(result.CaptureArea.Top, 3),
width = Math.Round(result.CaptureArea.Width, 3),
height = Math.Round(result.CaptureArea.Height, 3),
center_x = Math.Round(result.CaptureArea.CenterX, 3),
center_y = Math.Round(result.CaptureArea.CenterY, 3),
units = "meters"
} : null,
capture_type = result.CaptureType.ToString().ToLower(),
center_coordinates = centerCoordinates
}
};
}
else
{
return new
{
success = false,
error = result.ErrorMessage,
error_type = result.ErrorType
};
}
}
catch (Exception ex)
{
return new
{
success = false,
error = ex.Message,
error_type = ex.GetType().Name
};
}
}
/// <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 };
}
}
/// <summary>
/// Toma una captura de pantalla de objetos específicos por sus IDs usando el nuevo ScreenshotManager
/// </summary>
private object TakeObjectScreenshot(JObject arguments)
{
try
{
var objectIds = arguments["object_ids"]?.ToObject<string[]>();
if (objectIds == null || objectIds.Length == 0)
throw new ArgumentException("object_ids is required and must not be empty");
var paddingMeters = arguments["padding_meters"]?.ToObject<float>() ?? 0.5f;
var filename = arguments["filename"]?.ToString();
var includeBackground = arguments["include_background"]?.ToObject<bool>() ?? false;
var saveFile = arguments["save_file"]?.ToObject<bool>() ?? true;
var returnBase64 = arguments["return_base64"]?.ToObject<bool>() ?? true;
// Obtener ScreenshotManager de forma lazy y usar el nuevo método
var screenshotManager = GetScreenshotManager();
var result = screenshotManager.CaptureObjects(
objectIds, paddingMeters, filename, includeBackground, saveFile, returnBase64);
if (result.Success)
{
return new
{
success = true,
filename = result.FileName,
full_path = result.FilePath,
directory = result.Directory,
file_size_bytes = result.FileSizeBytes,
timestamp = result.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
message = "Object screenshot saved successfully",
base64_data = returnBase64 ? result.Base64Data : null,
captured_objects = result.CapturedObjects.Select(obj => new
{
id = obj.Id,
name = obj.Name,
type = obj.Type,
position = new { x = obj.Left, y = obj.Top },
center = new { x = obj.CenterX, y = obj.CenterY },
dimensions = new { width = obj.Width, height = obj.Height }
}).ToArray(),
bounding_box = new
{
left = Math.Round(result.BoundingBox.Left, 3),
top = Math.Round(result.BoundingBox.Top, 3),
right = Math.Round(result.BoundingBox.Left + result.BoundingBox.Width, 3),
bottom = Math.Round(result.BoundingBox.Top + result.BoundingBox.Height, 3),
center_x = Math.Round(result.BoundingBox.CenterX, 3),
center_y = Math.Round(result.BoundingBox.CenterY, 3),
width = Math.Round(result.BoundingBox.Width, 3),
height = Math.Round(result.BoundingBox.Height, 3)
},
capture_area = new
{
left = Math.Round(result.CaptureArea.Left, 3),
top = Math.Round(result.CaptureArea.Top, 3),
center_x = Math.Round(result.CaptureArea.CenterX, 3),
center_y = Math.Round(result.CaptureArea.CenterY, 3),
width = Math.Round(result.CaptureArea.Width, 3),
height = Math.Round(result.CaptureArea.Height, 3),
padding_meters = result.PaddingMeters
},
capture_info = new
{
include_background = result.IncludeBackground,
capture_type = result.CaptureType.ToString().ToLower(),
objects_count = result.CapturedObjects.Count
}
};
}
else
{
return new
{
success = false,
error = result.ErrorMessage,
error_type = result.ErrorType
};
}
}
catch (Exception ex)
{
return new
{
success = false,
error = ex.Message,
error_type = ex.GetType().Name
};
}
}
#endregion
#region Python Execution Support
/// <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))
";
// Create temporary script file and execute
var tempScript = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"mcp_python_{Guid.NewGuid()}.py");
try
{
await File.WriteAllTextAsync(tempScript, enhancedScript);
var result = await PythonInterop.ExecuteScriptAsync(tempScript);
if (result.Success)
{
// Parse return variables from output if available
var returnValues = new Dictionary<string, object>();
// Try to extract return_data from Python output
if (returnVariables.Length > 0 && result.Output.Contains("RETURN_DATA:"))
{
try
{
var jsonStart = result.Output.IndexOf("RETURN_DATA:") + "RETURN_DATA:".Length;
var jsonEnd = result.Output.IndexOf("\n", jsonStart);
if (jsonEnd == -1) jsonEnd = result.Output.Length;
var jsonStr = result.Output.Substring(jsonStart, jsonEnd - jsonStart).Trim();
var parsedData = JsonConvert.DeserializeObject<Dictionary<string, object>>(jsonStr);
if (parsedData != null)
{
returnValues = parsedData;
}
}
catch (Exception ex)
{
Debug.WriteLine($"[MCP Server] Error parsing return variables: {ex.Message}");
}
}
// Clean output to remove RETURN_DATA line
var cleanOutput = result.Output;
if (cleanOutput.Contains("RETURN_DATA:"))
{
var lines = cleanOutput.Split('\n');
cleanOutput = string.Join("\n", lines.Where(line => !line.StartsWith("RETURN_DATA:")));
}
return new
{
success = true,
output = cleanOutput?.Trim() ?? "",
variables = returnValues,
exit_code = result.ExitCode,
python_version = "CPython via PythonInterop"
};
}
else
{
return new
{
success = false,
error = result.Error ?? "Unknown error",
error_type = "PythonExecutionError",
output = result.Output,
exit_code = result.ExitCode
};
}
}
finally
{
// Clean up temporary script file
try
{
if (File.Exists(tempScript))
File.Delete(tempScript);
}
catch { }
}
}
catch (Exception ex)
{
return new
{
success = false,
error = ex.Message,
error_type = ex.GetType().Name
};
}
}
/// <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 screenshot tools instead"
};
break;
case "objects":
helpInfo["objects_help"] = new
{
description = "Collection of simulation objects",
usage = "Use get_objects() function for safe access",
note = "Direct object manipulation should use MCP tools"
};
break;
}
}
return new
{
success = true,
help = helpInfo
};
}
catch (Exception ex)
{
return new
{
success = false,
error = ex.Message
};
}
}
#endregion
#region Circular Debug Log System
/// <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}");
}
}
#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
}
}