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; }
}