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:
230
src/RhinoMcp.Plugin/RhinoMcpPlugin.cs
Normal file
230
src/RhinoMcp.Plugin/RhinoMcpPlugin.cs
Normal 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user