Initial project setup for RhinoMcp

- RhinoMcp.Plugin: Rhino 8 plugin with TCP listener on port 9744
- RhinoMcp.Bridge: MCP HTTP server on port 9743
- Basic MCP tools: ping, get_info, run_command, get_layers, get_objects
- Project documentation: CLAUDE.md, HANDOVER.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
architeur
2025-12-09 01:04:57 +01:00
commit 39f4ad9412
11 changed files with 1079 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
using Rhino;
using Rhino.PlugIns;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
namespace RhinoMcp.Plugin;
/// <summary>
/// RhinoMcp Plugin - Lauscht auf TCP-Befehle von der MCP Bridge
/// </summary>
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<string> ProcessRequestAsync(string requestJson)
{
try
{
var request = JsonSerializer.Deserialize<RhinoRequest>(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<string, object>? 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<string, object>? 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<string, object>? Args { get; set; }
}