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

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# Build results
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
# Rider
.idea/
# NuGet
*.nupkg
**/packages/*
# OS
.DS_Store
Thumbs.db
# Rhino Plugin output
*.rhp

76
CLAUDE.md Normal file
View File

@@ -0,0 +1,76 @@
# CLAUDE.md - RhinoMcp Projektanweisungen
> Diese Datei enthält Anweisungen für Claude Code bei der Arbeit an diesem Projekt.
## Projekt-Übersicht
RhinoMcp ist ein MCP-Server (Model Context Protocol) zur Fernsteuerung von Rhino 8.
Das Projekt besteht aus zwei Komponenten:
1. **RhinoMcp.Plugin** - Rhino 8 Plugin das auf Befehle lauscht
2. **RhinoMcp.Bridge** - Standalone MCP-Server der mit Claude kommuniziert
## Architektur
```
Claude Code ──HTTP/MCP──► RhinoMcp.Bridge ──TCP──► RhinoMcp.Plugin ──► Rhino 8
:9743 :9744 RhinoCommon API
```
## Build-Befehle
```bash
# Bridge bauen
cd src/RhinoMcp.Bridge
dotnet build
# Plugin bauen (benötigt Rhino 8 SDK)
cd src/RhinoMcp.Plugin
dotnet build
```
## Konventionen
### Code-Stil
- C# 12, .NET 8.0 für Bridge, .NET 7.0 für Plugin (Rhino 8 Kompatibilität)
- Nullable reference types aktiviert
- Deutsche Kommentare, englische Identifier
- Max 300 Zeilen pro Datei
### Datei-Organisation
```
src/
├── RhinoMcp.Plugin/ # Rhino 8 Plugin
│ ├── RhinoMcpPlugin.cs # Plugin Entry Point
│ ├── CommandListener.cs # TCP Server in Rhino
│ └── Commands/ # Rhino-Befehle
└── RhinoMcp.Bridge/ # MCP Server
├── Program.cs # Entry Point
└── Services/
├── McpServer.cs # HTTP MCP Server
├── RhinoConnection.cs # TCP Client zu Rhino
└── McpToolHandler.cs # Tool-Implementierungen
```
### MCP-Tool Namenskonvention
- Prefix: `rhino_`
- Beispiele: `rhino_run_command`, `rhino_get_objects`, `rhino_create_layer`
## Wichtige Dateien
- `CLAUDE.md` - Diese Datei (Anweisungen für Claude)
- `HANDOVER.md` - Session-Übergabe Status
- `src/RhinoMcp.Bridge/Services/McpServer.cs` - MCP HTTP Server
- `src/RhinoMcp.Plugin/RhinoMcpPlugin.cs` - Rhino Plugin Entry
## Git & Deployment
- Repository: `ssh://git@git.artetui.de:22222/admin/RhinoMcp.git`
- Branch: `master`
- Commit-Messages auf Englisch mit Emoji-Prefix
## Rhino 8 Spezifika
- RhinoCommon NuGet: `RhinoCommon` (Version 8.x)
- Plugin GUID muss eindeutig sein
- Plugin wird nach `%APPDATA%\McNeel\Rhinoceros\8.0\Plug-ins\` kopiert

53
HANDOVER.md Normal file
View File

@@ -0,0 +1,53 @@
# Session-Übergabe
> Diese Datei wird bei jeder Übergabe ÜBERSCHRIEBEN, nicht ergänzt.
## Letztes Update
- Datum: 2025-12-09
- Grund: Projekt-Initialisierung
## Aktueller Stand
- Projekt-Grundgerüst erstellt
- Git-Repository initialisiert
- CLAUDE.md mit Projektkonventionen erstellt
## Architektur
```
Claude Code ──HTTP/MCP──► RhinoMcp.Bridge ──TCP──► RhinoMcp.Plugin ──► Rhino 8
:9743 :9744 RhinoCommon API
```
## Projektstruktur
```
C:\RhinoMcp\
├── CLAUDE.md # Projektanweisungen für Claude
├── HANDOVER.md # Diese Datei
├── docs/ # Dokumentation
└── src/
├── RhinoMcp.Plugin/ # Rhino 8 Plugin (C#, .NET 7.0)
└── RhinoMcp.Bridge/ # MCP Server (C#, .NET 8.0)
```
## Nächste Schritte
1. [ ] RhinoMcp.Bridge Projekt erstellen mit MCP-Server
2. [ ] RhinoMcp.Plugin Projekt erstellen mit TCP-Listener
3. [ ] Erste MCP-Tools implementieren (rhino_run_command, rhino_get_info)
4. [ ] Plugin in Rhino testen
5. [ ] Gitea Repository erstellen und pushen
## Build-Befehle
```bash
cd src/RhinoMcp.Bridge && dotnet build
cd src/RhinoMcp.Plugin && dotnet build
```
## Git
- Remote: `ssh://git@git.artetui.de:22222/admin/RhinoMcp.git`
- Branch: `master`

View File

@@ -0,0 +1,22 @@
using RhinoMcp.Bridge.Services;
namespace RhinoMcp.Bridge;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("RhinoMcp Bridge startet...");
var mcpServer = new McpServer();
var rhinoConnection = new RhinoConnection();
// Rhino-Verbindung dem MCP-Server bekannt machen
McpToolHandler.Initialize(rhinoConnection);
// Server starten
await mcpServer.StartAsync();
Console.WriteLine("RhinoMcp Bridge beendet.");
}
}

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,221 @@
using System.Net;
using System.Text;
using System.Text.Json;
namespace RhinoMcp.Bridge.Services;
/// <summary>
/// MCP (Model Context Protocol) Server für Claude Code Integration mit Rhino 8.
/// Läuft auf localhost:9743
/// </summary>
public class McpServer : IDisposable
{
private readonly HttpListener _listener;
private CancellationTokenSource? _cts;
private Task? _serverTask;
public const int DefaultPort = 9743;
public bool IsRunning { get; private set; }
public string BaseUrl => $"http://localhost:{DefaultPort}/";
public McpServer()
{
_listener = new HttpListener();
_listener.Prefixes.Add(BaseUrl);
}
/// <summary>
/// Startet den MCP-Server und wartet bis Ctrl+C gedrückt wird.
/// </summary>
public async Task StartAsync()
{
if (IsRunning) return;
try
{
_listener.Start();
_cts = new CancellationTokenSource();
IsRunning = true;
Console.WriteLine($"[MCP] Server gestartet auf {BaseUrl}");
Console.WriteLine("[MCP] Drücke Ctrl+C zum Beenden");
// Ctrl+C Handler
Console.CancelKeyPress += (s, e) =>
{
e.Cancel = true;
_cts.Cancel();
};
await ListenAsync(_cts.Token);
}
catch (Exception ex)
{
Console.WriteLine($"[MCP] Fehler beim Starten: {ex.Message}");
}
finally
{
Stop();
}
}
/// <summary>
/// Stoppt den MCP-Server.
/// </summary>
public void Stop()
{
if (!IsRunning) return;
_cts?.Cancel();
_listener.Stop();
IsRunning = false;
Console.WriteLine("[MCP] Server gestoppt");
}
private async Task ListenAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested && _listener.IsListening)
{
try
{
var context = await _listener.GetContextAsync();
_ = Task.Run(() => HandleRequestAsync(context), ct);
}
catch (HttpListenerException) when (ct.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
Console.WriteLine($"[MCP] Listener-Fehler: {ex.Message}");
}
}
}
private async Task HandleRequestAsync(HttpListenerContext context)
{
var request = context.Request;
var response = context.Response;
// CORS Headers
response.Headers.Add("Access-Control-Allow-Origin", "*");
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
try
{
if (request.HttpMethod == "OPTIONS")
{
response.StatusCode = 200;
response.Close();
return;
}
var path = request.Url?.AbsolutePath ?? "/";
var result = path switch
{
"/" => GetServerInfo(),
"/tools" => GetToolList(),
"/call" => await HandleToolCall(request),
_ => new McpResponse { Error = $"Unknown path: {path}" }
};
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
var buffer = Encoding.UTF8.GetBytes(json);
response.ContentType = "application/json";
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
}
catch (Exception ex)
{
var error = JsonSerializer.Serialize(new McpResponse { Error = ex.Message });
var buffer = Encoding.UTF8.GetBytes(error);
response.StatusCode = 500;
response.ContentType = "application/json";
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
}
finally
{
response.Close();
}
}
private McpResponse GetServerInfo()
{
return new McpResponse
{
Success = true,
Data = new
{
name = "RhinoMcp Bridge",
version = "1.0.0",
description = "MCP-Schnittstelle für Rhino 8 - Geometrie, Layer, Befehle",
endpoints = new[] { "/", "/tools", "/call" },
rhinoPort = 9744
}
};
}
private McpResponse GetToolList()
{
return new McpResponse
{
Success = true,
Data = McpToolDefinitions.AllTools
};
}
private async Task<McpResponse> HandleToolCall(HttpListenerRequest request)
{
if (request.HttpMethod != "POST")
{
return new McpResponse { Error = "POST required for /call" };
}
using var reader = new StreamReader(request.InputStream, request.ContentEncoding);
var body = await reader.ReadToEndAsync();
var call = JsonSerializer.Deserialize<McpToolCall>(body, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (call == null || string.IsNullOrEmpty(call.Tool))
{
return new McpResponse { Error = "Invalid tool call" };
}
return await McpToolHandler.ExecuteAsync(call);
}
public void Dispose()
{
Stop();
_listener.Close();
_cts?.Dispose();
}
}
/// <summary>
/// MCP Response-Struktur
/// </summary>
public class McpResponse
{
public bool Success { get; set; }
public string? Error { get; set; }
public object? Data { get; set; }
}
/// <summary>
/// MCP Tool-Aufruf
/// </summary>
public class McpToolCall
{
public string Tool { get; set; } = "";
public Dictionary<string, JsonElement>? Args { get; set; }
}

View File

@@ -0,0 +1,142 @@
namespace RhinoMcp.Bridge.Services;
/// <summary>
/// MCP Tool-Definitionen für Rhino 8
/// </summary>
public static class McpToolDefinitions
{
public static readonly List<McpToolDef> AllTools = new()
{
// === VERBINDUNG ===
new McpToolDef
{
Name = "rhino_ping",
Description = "Prüft die Verbindung zu Rhino 8",
Parameters = new()
},
new McpToolDef
{
Name = "rhino_get_info",
Description = "Gibt Informationen über Rhino und das aktive Dokument zurück",
Parameters = new()
},
// === BEFEHLE ===
new McpToolDef
{
Name = "rhino_run_command",
Description = "Führt einen Rhino-Befehl aus (wie in der Kommandozeile)",
Parameters = new()
{
["command"] = new ParamDef
{
Type = "string",
Description = "Der auszuführende Rhino-Befehl (z.B. '_Line', '_Circle 0,0,0 10')",
Required = true
}
}
},
new McpToolDef
{
Name = "rhino_run_script",
Description = "Führt ein Python- oder GH-Script in Rhino aus",
Parameters = new()
{
["script"] = new ParamDef
{
Type = "string",
Description = "Der Script-Code",
Required = true
},
["language"] = new ParamDef
{
Type = "string",
Description = "Script-Sprache: python (default), ghpython"
}
}
},
// === LAYER ===
new McpToolDef
{
Name = "rhino_get_layers",
Description = "Gibt alle Layer im aktiven Dokument zurück",
Parameters = new()
},
new McpToolDef
{
Name = "rhino_create_layer",
Description = "Erstellt einen neuen Layer",
Parameters = new()
{
["name"] = new ParamDef
{
Type = "string",
Description = "Name des neuen Layers",
Required = true
},
["color"] = new ParamDef
{
Type = "string",
Description = "Farbe als Hex-Code (z.B. #FF0000)"
}
}
},
// === OBJEKTE ===
new McpToolDef
{
Name = "rhino_get_objects",
Description = "Gibt Objekte im Dokument zurück (optional gefiltert)",
Parameters = new()
{
["layer"] = new ParamDef
{
Type = "string",
Description = "Filter nach Layer-Name"
},
["type"] = new ParamDef
{
Type = "string",
Description = "Filter nach Objekttyp (Curve, Surface, Brep, Mesh, etc.)"
}
}
},
new McpToolDef
{
Name = "rhino_select_objects",
Description = "Selektiert Objekte nach Kriterien",
Parameters = new()
{
["layer"] = new ParamDef
{
Type = "string",
Description = "Selektiere alle Objekte auf diesem Layer"
},
["type"] = new ParamDef
{
Type = "string",
Description = "Selektiere alle Objekte dieses Typs"
}
}
}
};
}
public class McpToolDef
{
public string Name { get; set; } = "";
public string Description { get; set; } = "";
public Dictionary<string, ParamDef> Parameters { get; set; } = new();
}
public class ParamDef
{
public string Type { get; set; } = "string";
public string Description { get; set; } = "";
public bool Required { get; set; }
}

View File

@@ -0,0 +1,172 @@
using System.Text.Json;
namespace RhinoMcp.Bridge.Services;
/// <summary>
/// Handler für MCP Tool-Aufrufe - leitet an Rhino weiter
/// </summary>
public static class McpToolHandler
{
private static RhinoConnection? _rhino;
public static void Initialize(RhinoConnection rhinoConnection)
{
_rhino = rhinoConnection;
}
public static async Task<McpResponse> ExecuteAsync(McpToolCall call)
{
if (_rhino == null)
{
return new McpResponse { Error = "RhinoConnection nicht initialisiert" };
}
try
{
return call.Tool switch
{
"rhino_ping" => await HandlePing(),
"rhino_get_info" => await HandleGetInfo(),
"rhino_run_command" => await HandleRunCommand(call.Args),
"rhino_get_layers" => await HandleGetLayers(),
"rhino_get_objects" => await HandleGetObjects(call.Args),
"rhino_create_layer" => await HandleCreateLayer(call.Args),
"rhino_select_objects" => await HandleSelectObjects(call.Args),
"rhino_run_script" => await HandleRunScript(call.Args),
_ => new McpResponse { Error = $"Unbekanntes Tool: {call.Tool}" }
};
}
catch (Exception ex)
{
return new McpResponse { Error = ex.Message };
}
}
private static async Task<McpResponse> HandlePing()
{
var result = await _rhino!.SendCommandAsync("ping");
return WrapResult(result);
}
private static async Task<McpResponse> HandleGetInfo()
{
var result = await _rhino!.SendCommandAsync("get_info");
return WrapResult(result);
}
private static async Task<McpResponse> HandleRunCommand(Dictionary<string, JsonElement>? args)
{
var command = GetStringArg(args, "command");
if (string.IsNullOrEmpty(command))
{
return new McpResponse { Error = "command parameter required" };
}
var result = await _rhino!.SendCommandAsync("run_command", new Dictionary<string, object>
{
["command"] = command
});
return WrapResult(result);
}
private static async Task<McpResponse> HandleGetLayers()
{
var result = await _rhino!.SendCommandAsync("get_layers");
return WrapResult(result);
}
private static async Task<McpResponse> HandleGetObjects(Dictionary<string, JsonElement>? args)
{
var rhinoArgs = new Dictionary<string, object>();
var layer = GetStringArg(args, "layer");
if (!string.IsNullOrEmpty(layer))
rhinoArgs["layer"] = layer;
var type = GetStringArg(args, "type");
if (!string.IsNullOrEmpty(type))
rhinoArgs["type"] = type;
var result = await _rhino!.SendCommandAsync("get_objects", rhinoArgs);
return WrapResult(result);
}
private static async Task<McpResponse> HandleCreateLayer(Dictionary<string, JsonElement>? args)
{
var name = GetStringArg(args, "name");
if (string.IsNullOrEmpty(name))
{
return new McpResponse { Error = "name parameter required" };
}
var rhinoArgs = new Dictionary<string, object> { ["name"] = name };
var color = GetStringArg(args, "color");
if (!string.IsNullOrEmpty(color))
rhinoArgs["color"] = color;
var result = await _rhino!.SendCommandAsync("create_layer", rhinoArgs);
return WrapResult(result);
}
private static async Task<McpResponse> HandleSelectObjects(Dictionary<string, JsonElement>? args)
{
var rhinoArgs = new Dictionary<string, object>();
var layer = GetStringArg(args, "layer");
if (!string.IsNullOrEmpty(layer))
rhinoArgs["layer"] = layer;
var type = GetStringArg(args, "type");
if (!string.IsNullOrEmpty(type))
rhinoArgs["type"] = type;
var result = await _rhino!.SendCommandAsync("select_objects", rhinoArgs);
return WrapResult(result);
}
private static async Task<McpResponse> HandleRunScript(Dictionary<string, JsonElement>? args)
{
var script = GetStringArg(args, "script");
if (string.IsNullOrEmpty(script))
{
return new McpResponse { Error = "script parameter required" };
}
var language = GetStringArg(args, "language") ?? "python";
var result = await _rhino!.SendCommandAsync("run_script", new Dictionary<string, object>
{
["script"] = script,
["language"] = language
});
return WrapResult(result);
}
#region Helpers
private static string? GetStringArg(Dictionary<string, JsonElement>? args, string key)
{
if (args == null || !args.TryGetValue(key, out var element))
return null;
return element.ValueKind == JsonValueKind.String ? element.GetString() : element.ToString();
}
private static McpResponse WrapResult(JsonElement? result)
{
if (result == null)
{
return new McpResponse { Error = "Keine Verbindung zu Rhino - ist Rhino 8 gestartet und das Plugin geladen?" };
}
if (result.Value.TryGetProperty("error", out var errorProp))
{
return new McpResponse { Error = errorProp.GetString() };
}
return new McpResponse { Success = true, Data = result };
}
#endregion
}

View File

@@ -0,0 +1,107 @@
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
namespace RhinoMcp.Bridge.Services;
/// <summary>
/// TCP-Verbindung zum RhinoMcp Plugin in Rhino 8
/// </summary>
public class RhinoConnection : IDisposable
{
private TcpClient? _client;
private NetworkStream? _stream;
private readonly object _lock = new();
public const string Host = "127.0.0.1";
public const int Port = 9744;
public bool IsConnected => _client?.Connected ?? false;
/// <summary>
/// Verbindet zum Rhino Plugin
/// </summary>
public bool Connect()
{
lock (_lock)
{
try
{
Disconnect();
_client = new TcpClient();
_client.Connect(Host, Port);
_stream = _client.GetStream();
Console.WriteLine($"[Rhino] Verbunden mit Rhino auf Port {Port}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"[Rhino] Verbindungsfehler: {ex.Message}");
return false;
}
}
}
/// <summary>
/// Trennt die Verbindung
/// </summary>
public void Disconnect()
{
lock (_lock)
{
_stream?.Dispose();
_client?.Dispose();
_stream = null;
_client = null;
}
}
/// <summary>
/// Sendet einen Befehl an Rhino und wartet auf Antwort
/// </summary>
public async Task<JsonElement?> SendCommandAsync(string command, Dictionary<string, object>? args = null)
{
// Bei Bedarf verbinden
if (!IsConnected && !Connect())
{
return null;
}
var request = new
{
Command = command,
Args = args
};
var json = JsonSerializer.Serialize(request);
var buffer = Encoding.UTF8.GetBytes(json);
try
{
await _stream!.WriteAsync(buffer);
// Antwort lesen
var responseBuffer = new byte[65536];
var bytesRead = await _stream.ReadAsync(responseBuffer);
if (bytesRead == 0)
{
Disconnect();
return null;
}
var responseJson = Encoding.UTF8.GetString(responseBuffer, 0, bytesRead).Trim();
return JsonSerializer.Deserialize<JsonElement>(responseJson);
}
catch (Exception ex)
{
Console.WriteLine($"[Rhino] Kommunikationsfehler: {ex.Message}");
Disconnect();
return null;
}
}
public void Dispose()
{
Disconnect();
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDynamicLoading>true</EnableDynamicLoading>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RhinoCommon" Version="8.0.23304.9001">
<PrivateAssets>all</PrivateAssets>
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</Project>

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