using Rhino; using Rhino.PlugIns; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.Json; namespace RhinoMcp.Plugin; /// /// RhinoMcp Plugin - Lauscht auf TCP-Befehle von der MCP Bridge /// public class RhinoMcpPlugin : PlugIn { private TcpListener? _listener; private CancellationTokenSource? _cts; private const int DefaultPort = 9744; public static RhinoMcpPlugin? Instance { get; private set; } public RhinoMcpPlugin() { Instance = this; } public override PlugInLoadTime LoadTime => PlugInLoadTime.AtStartup; protected override LoadReturnCode OnLoad(ref string errorMessage) { RhinoApp.WriteLine("RhinoMcp Plugin wird geladen..."); // TCP Server starten StartTcpServer(); RhinoApp.WriteLine($"RhinoMcp Plugin aktiv auf Port {DefaultPort}"); return LoadReturnCode.Success; } protected override void OnShutdown() { StopTcpServer(); base.OnShutdown(); } private void StartTcpServer() { _cts = new CancellationTokenSource(); _listener = new TcpListener(IPAddress.Loopback, DefaultPort); _listener.Start(); Task.Run(async () => await AcceptClientsAsync(_cts.Token)); } private void StopTcpServer() { _cts?.Cancel(); _listener?.Stop(); } private async Task AcceptClientsAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { var client = await _listener!.AcceptTcpClientAsync(ct); _ = HandleClientAsync(client, ct); } catch (OperationCanceledException) { break; } catch (Exception ex) { RhinoApp.WriteLine($"RhinoMcp Fehler: {ex.Message}"); } } } private async Task HandleClientAsync(TcpClient client, CancellationToken ct) { using (client) await using (var stream = client.GetStream()) { var buffer = new byte[8192]; while (!ct.IsCancellationRequested && client.Connected) { try { var bytesRead = await stream.ReadAsync(buffer, ct); if (bytesRead == 0) break; var request = Encoding.UTF8.GetString(buffer, 0, bytesRead); var response = await ProcessRequestAsync(request); var responseBytes = Encoding.UTF8.GetBytes(response + "\n"); await stream.WriteAsync(responseBytes, ct); } catch (Exception ex) { RhinoApp.WriteLine($"RhinoMcp Client-Fehler: {ex.Message}"); break; } } } } private Task ProcessRequestAsync(string requestJson) { try { var request = JsonSerializer.Deserialize(requestJson); if (request == null) return Task.FromResult(JsonSerializer.Serialize(new { error = "Invalid request" })); // Befehle auf dem UI-Thread ausführen object? result = null; Exception? error = null; RhinoApp.InvokeOnUiThread(() => { try { result = ExecuteCommand(request); } catch (Exception ex) { error = ex; } }); if (error != null) return Task.FromResult(JsonSerializer.Serialize(new { error = error.Message })); return Task.FromResult(JsonSerializer.Serialize(new { success = true, result })); } catch (Exception ex) { return Task.FromResult(JsonSerializer.Serialize(new { error = ex.Message })); } } private object? ExecuteCommand(RhinoRequest request) { return request.Command?.ToLower() switch { "ping" => new { message = "pong", version = RhinoApp.Version.ToString() }, "run_command" => RunRhinoCommand(request.Args), "get_info" => GetRhinoInfo(), "get_layers" => GetLayers(), "get_objects" => GetObjects(request.Args), _ => throw new ArgumentException($"Unbekannter Befehl: {request.Command}") }; } private object RunRhinoCommand(Dictionary? args) { var command = args?.GetValueOrDefault("command")?.ToString(); if (string.IsNullOrEmpty(command)) throw new ArgumentException("command parameter required"); var result = RhinoApp.RunScript(command, echo: false); return new { executed = result, command }; } private object GetRhinoInfo() { var doc = RhinoDoc.ActiveDoc; return new { version = RhinoApp.Version.ToString(), document = doc?.Name ?? "(kein Dokument)", units = doc?.ModelUnitSystem.ToString() ?? "unknown", objectCount = doc?.Objects.Count ?? 0, layerCount = doc?.Layers.Count ?? 0 }; } private object GetLayers() { var doc = RhinoDoc.ActiveDoc; if (doc == null) return new { error = "Kein aktives Dokument" }; var layers = doc.Layers .Where(l => !l.IsDeleted) .Select(l => new { id = l.Id, name = l.Name, fullPath = l.FullPath, visible = l.IsVisible, locked = l.IsLocked, color = $"#{l.Color.R:X2}{l.Color.G:X2}{l.Color.B:X2}" }) .ToList(); return new { count = layers.Count, layers }; } private object GetObjects(Dictionary? args) { var doc = RhinoDoc.ActiveDoc; if (doc == null) return new { error = "Kein aktives Dokument" }; var layerFilter = args?.GetValueOrDefault("layer")?.ToString(); var typeFilter = args?.GetValueOrDefault("type")?.ToString(); var objects = doc.Objects .Where(o => !o.IsDeleted) .Where(o => layerFilter == null || doc.Layers[o.Attributes.LayerIndex].Name == layerFilter) .Select(o => new { id = o.Id, type = o.ObjectType.ToString(), layer = doc.Layers[o.Attributes.LayerIndex].Name, name = o.Attributes.Name ?? "" }) .Take(100) // Limitieren um Performance zu wahren .ToList(); return new { count = objects.Count, objects }; } } public class RhinoRequest { public string? Command { get; set; } public Dictionary? Args { get; set; } }