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:
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
76
CLAUDE.md
Normal 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
53
HANDOVER.md
Normal 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`
|
||||||
22
src/RhinoMcp.Bridge/Program.cs
Normal file
22
src/RhinoMcp.Bridge/Program.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/RhinoMcp.Bridge/RhinoMcp.Bridge.csproj
Normal file
11
src/RhinoMcp.Bridge/RhinoMcp.Bridge.csproj
Normal 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>
|
||||||
221
src/RhinoMcp.Bridge/Services/McpServer.cs
Normal file
221
src/RhinoMcp.Bridge/Services/McpServer.cs
Normal 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; }
|
||||||
|
}
|
||||||
142
src/RhinoMcp.Bridge/Services/McpToolDefinitions.cs
Normal file
142
src/RhinoMcp.Bridge/Services/McpToolDefinitions.cs
Normal 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; }
|
||||||
|
}
|
||||||
172
src/RhinoMcp.Bridge/Services/McpToolHandler.cs
Normal file
172
src/RhinoMcp.Bridge/Services/McpToolHandler.cs
Normal 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
|
||||||
|
}
|
||||||
107
src/RhinoMcp.Bridge/Services/RhinoConnection.cs
Normal file
107
src/RhinoMcp.Bridge/Services/RhinoConnection.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/RhinoMcp.Plugin/RhinoMcp.Plugin.csproj
Normal file
18
src/RhinoMcp.Plugin/RhinoMcp.Plugin.csproj
Normal 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>
|
||||||
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