This is page 1 of 7. Use http://codebase.md/jingcheng-chen/rhinomcp?page={x} to view the full context.
# Directory Structure
```
├── .github
│ └── workflows
│ ├── mcp-server-publish.yml
│ └── rhino-plugin-publish.yml
├── .gitignore
├── assets
│ ├── claude_enable_instruction.jpg
│ ├── cursor_enable_instruction.jpg
│ ├── cursor_usage_instruction.jpg
│ ├── demo1.jpg
│ ├── demo2.jpg
│ ├── rhino_plugin_instruction.jpg
│ └── rhinomcp_logo.svg
├── demo_chats
│ ├── create_6x6x6_boxes.txt
│ └── create_rhinoceros_lego_blocks.txt
├── LICENSE
├── README.md
├── rhino_mcp_plugin
│ ├── .gitignore
│ ├── Commands
│ │ ├── MCPStartCommand.cs
│ │ ├── MCPStopCommand.cs
│ │ └── MCPVersionCommand.cs
│ ├── EmbeddedResources
│ │ └── plugin-utility.ico
│ ├── Functions
│ │ ├── _utils.cs
│ │ ├── CreateLayer.cs
│ │ ├── CreateObject.cs
│ │ ├── CreateObjects.cs
│ │ ├── DeleteLayer.cs
│ │ ├── DeleteObject.cs
│ │ ├── ExecuteRhinoscript.cs
│ │ ├── GetDocumentInfo.cs
│ │ ├── GetObjectInfo.cs
│ │ ├── GetOrSetCurrentLayer.cs
│ │ ├── GetSelectedObjectsInfo.cs
│ │ ├── ModifyObject.cs
│ │ ├── ModifyObjects.cs
│ │ └── SelectObjects.cs
│ ├── manifest.yml
│ ├── Properties
│ │ ├── AssemblyInfo.cs
│ │ └── launchSettings.json
│ ├── rhinomcp.csproj
│ ├── rhinomcp.sln
│ ├── RhinoMCPPlugin.cs
│ ├── RhinoMCPServer.cs
│ ├── RhinoMCPServerController.cs
│ └── Serializers
│ └── Serializer.cs
└── rhino_mcp_server
├── .gitignore
├── dev.sh
├── main.py
├── pyproject.toml
├── README.md
├── src
│ └── rhinomcp
│ ├── __init__.py
│ ├── prompts
│ │ └── assert_general_strategy.py
│ ├── server.py
│ ├── static
│ │ └── rhinoscriptsyntax.py
│ └── tools
│ ├── create_layer.py
│ ├── create_object.py
│ ├── create_objects.py
│ ├── delete_layer.py
│ ├── delete_object.py
│ ├── execute_rhinoscript_python_code.py
│ ├── get_document_info.py
│ ├── get_object_info.py
│ ├── get_or_set_current_layer.py
│ ├── get_rhinoscript_python_code_guide.py
│ ├── get_rhinoscript_python_function_names.py
│ ├── get_selected_objects_info.py
│ ├── modify_object.py
│ ├── modify_objects.py
│ └── select_objects.py
├── static
│ ├── application.py
│ ├── block.py
│ ├── compat.py
│ ├── curve.py
│ ├── dimension.py
│ ├── document.py
│ ├── geometry.py
│ ├── grips.py
│ ├── group.py
│ ├── hatch.py
│ ├── layer.py
│ ├── light.py
│ ├── line.py
│ ├── linetype.py
│ ├── material.py
│ ├── mesh.py
│ ├── object.py
│ ├── plane.py
│ ├── pointvector.py
│ ├── selection.py
│ ├── surface.py
│ ├── toolbar.py
│ ├── transformation.py
│ ├── userdata.py
│ ├── userinterface.py
│ ├── utility.py
│ └── view.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
.DS_Store
.vscode
.cursor
node_modules/
```
--------------------------------------------------------------------------------
/rhino_mcp_server/.gitignore:
--------------------------------------------------------------------------------
```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Virtual environments
venv/
.env/
.venv/
env/
pip-wheel-metadata/
pip-log.txt
# Package metadata
*.egg-info/
*.dist-info/
.installed.cfg
# Build artifacts
build/
dist/
wheelhouse/
# Testing & coverage
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
pytest_cache/
nosetests.xml
coverage.xml
*.cover
# Documentation
docs/_build/
*.rST~
# Jupyter Notebook checkpoints
.ipynb_checkpoints/
# MyPy & type-checking
.mypy_cache/
.dmypy.json
pyre-check/
# IDE / Editor files
.vscode/
.idea/
*.sublime-workspace
*.sublime-project
# Logs & Debugging
logs/
*.log
*.out
*.err
debug.log
# Local environment settings
.env
.envrc
*.local
*.secret
# Docker
.dockerignore
docker-compose.override.yml
# Rye, Poetry, and Pipenv lock files
.poetry/
rye.lock
Pipfile
Pipfile.lock
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/.gitignore:
--------------------------------------------------------------------------------
```
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
*.idea
```
--------------------------------------------------------------------------------
/rhino_mcp_server/README.md:
--------------------------------------------------------------------------------
```markdown
# RhinoMCP - Rhino Model Context Protocol Integration
RhinoMCP connects Rhino to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control Rhino. This integration enables prompt assisted 3D modeling in Rhino 3D.
Please visit Github for complete information:
[Github](https://github.com/jingcheng-chen/rhinomcp)
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# RhinoMCP - Rhino Model Context Protocol Integration
<img src="assets/rhinomcp_logo.svg" alt="RhinoMCP Logo" width="130">
RhinoMCP connects Rhino to AI agents through the Model Context Protocol (MCP), allowing AI agents to directly interact with and control Rhino. This integration enables prompt assisted 3D modeling in Rhino 3D.
## Features
- **Two-way communication**: Connect AI agents to Rhino through a socket-based server
- **Object manipulation**: Create, modify, and delete 3D objects in Rhino
- **Document inspection**: Get detailed information about the current Rhino document
- **Script execution**: Execute Rhinos python scripts in Rhino (experimental, may not work every time)
- **Get Script Documentation**: Get the documentation of a specific RhinoScript python function
- **Object selection**: Select objects based on filters, e.g. name, color, category, etc. with "and" or "or" logic
- **Set/Create/Delete Layers**: Get or set the current layer, create new layers, or delete layers
> [!NOTE]
> So far the tool only supports creating primitive objects for proof of concept. More geometries will be added in the future.
> Supported objects: Point, Line, Polyline, Circle, Arc, Ellipse, Curve, Box, Sphere, Cone, Cylinder, Surface (from points)
## Demo
### Demo 1
This demo shows how AI can interact with Rhino in two directions. Click the image below to watch the video.
[](https://youtu.be/pi6dbqUuhI4)
### Demo 2
This demo shows how to ask AI to create custom scripts and execute them in Rhino. Click the image below to watch the video.
[](https://youtu.be/NFOF_Pjp3qY)
## Tutorial
Thanks to Nate. He has created a showcase and installation [tutorial](https://www.youtube.com/watch?v=z2IBP81ABRM) for this tool.
## Components
The system consists of two main components:
1. **MCP Server (`src/rhino_mcp_server/server.py`)**: A Python server that implements the Model Context Protocol and connects to the Rhino plugin
2. **Rhino Plugin (`src/rhino_mcp_plugin`)**: A Rhino plugin that creates a socket server within Rhino to receive and execute commands
## Installation
### Prerequisites
- Rhino 7 or newer (Works onWindows and Mac); make sure you Rhino is up to date.
- Python 3.10 or newer
- uv package manager
**⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both**
### Installing the Rhino Plugin
1. Go to Tools > Package Manager
2. Search for `rhinomcp`
3. Click `Install`
#### Install uv
**If you're on Mac, please install uv as**
```bash
brew install uv
```
**On Windows**
```bash
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
```
**⚠️ Do not proceed before installing UV**
### Config file
```json
{
"mcpServers": {
"rhino": {
"command": "uvx",
"args": ["rhinomcp"]
}
}
}
```
### Claude for Desktop Integration
Go to Claude > Settings > Developer > Edit Config > claude_desktop_config.json to include the above config file.
### Cursor integration
Make sure your cursor is up to date.
Create a folder `.cursor` in your project root.
Create a file `mcp.json` in the `.cursor` folder and include the above config file:
Go to Cursor Settings > MCP and check if it's enabled.
## Usage
### Starting the Connection

1. In Rhino, type `mcpstart` in the command line
2. Make sure the MCP server is running in the rhino terminal
### Using with Claude
Once the config file has been set on Claude, and the plugin is running on Rhino, you will see a hammer icon with tools for the RhinoMCP.

### Using with Cursor
Once the config file has been set on Cursor, and the plugin is running on Rhino, you will see the green indicator in front of the MCP server.

If not, try refresh the server in Cursor. If any console pops up, please do not close it.
Once it's ready, use `Ctrl+I` to open the chat box and start chatting with Rhino. Make sure you've selected **Agent** mode.

## Technical Details
### Communication Protocol
The system uses a simple JSON-based protocol over TCP sockets:
- **Commands** are sent as JSON objects with a `type` and optional `params`
- **Responses** are JSON objects with a `status` and `result` or `message`
## Limitations & Security Considerations
- The `get_document_info` only fetches max 30 objects, layers, material etc. to avoid huge dataset that overwhelms Claude.
- Complex operations might need to be broken down into smaller steps
## Building the tool and publishing
### Building and publishing the server
```bash
cd rhino_mcp_server
uv build
uv publish
```
### Building and publishing the plugin
1. build the tool in Release mode
2. copy the "manifest.yml" file to the "bin/Release" folder
3. run `yak build` in the Release folder
4. run `yak push rhino_mcp_plugin_xxxx.yak` to publish the plugin
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Disclaimer
This is a third-party integration and not made by Mcneel. Made by [Jingcheng Chen](https://github.com/jingcheng-chen)
## Star History
[](https://www.star-history.com/#jingcheng-chen/rhinomcp&Date)
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/compat.py:
--------------------------------------------------------------------------------
```python
```
--------------------------------------------------------------------------------
/rhino_mcp_server/dev.sh:
--------------------------------------------------------------------------------
```bash
#! /bin/bash
# Dev the server
uv venv
uv run mcp dev main.py:mcp
```
--------------------------------------------------------------------------------
/rhino_mcp_server/main.py:
--------------------------------------------------------------------------------
```python
from rhinomcp.server import main as server_main
from rhinomcp.server import mcp
def main():
"""Entry point for the rhinomcp package"""
server_main()
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/demo_chats/create_rhinoceros_lego_blocks.txt:
--------------------------------------------------------------------------------
```
1. Please create a Rhinoceros animal that is composed of cubic blocks. You could colorize the blocks with some carton colors.
2. Please change the head to red color.
3. Rotate selected object 90 degrees around the Z axis.
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/manifest.yml:
--------------------------------------------------------------------------------
```yaml
name: rhinomcp
version: 0.1.3.6
authors:
- Jingcheng Chen
description: Rhino integration through the Model Context Protocol
keywords:
- rhinomcp
- mcp
- rhino
- ai
- prompt
- modelcontextprotocol
url: 'https://github.com/jingcheng-chen/rhinomcp'
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/GetObjectInfo.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using Newtonsoft.Json.Linq;
using Rhino;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject GetObjectInfo(JObject parameters)
{
var obj = getObjectByIdOrName(parameters);
var data = Serializer.RhinoObject(obj);
data["attributes"] = Serializer.RhinoObjectAttributes(obj);
return data;
}
}
```
--------------------------------------------------------------------------------
/demo_chats/create_6x6x6_boxes.txt:
--------------------------------------------------------------------------------
```
Please create 6x6x6 boxes. Their position are on a 6x6x6 grid. Grid start from the origin (0,0,0) and expand to x,y,z positive directions. Grid size is 10. Box dimension vary from 1-5, starting with 1 at the origin and gradually ends to 5 at the opposite top most corner. They also have a gradient color from blue (small) to red (large) based on their size. Please try to use rhinoscript python code to create the boxes.
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/get_document_info.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp import get_rhino_connection, mcp, logger
@mcp.tool()
def get_document_info(ctx: Context) -> str:
"""Get detailed information about the current Rhino document"""
try:
rhino = get_rhino_connection()
result = rhino.send_command("get_document_info")
# Just return the JSON representation of what Rhino sent us
return json.dumps(result, indent=2)
except Exception as e:
logger.error(f"Error getting document info from Rhino: {str(e)}")
return f"Error getting document info: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Properties/launchSettings.json:
--------------------------------------------------------------------------------
```json
{
"profiles": {
"Rhino 8 - netcore (Windows)": {
"commandName": "Executable",
"executablePath": "C:\\Program Files\\Rhino 8\\System\\Rhino.exe",
"commandLineArgs": "/netcore",
"environmentVariables": {
"RHINO_PACKAGE_DIRS": "$(ProjectDir)$(OutputPath)\\"
},
"nativeDebugging": true
},
"Rhino 8 - netcore (macOS)": {
"commandName": "Executable",
"executablePath": "/Applications/Rhino 8.app/Contents/MacOS/Rhinoceros",
"environmentVariables": {
"RHINO_PACKAGE_DIRS": "${workspaceFolder}/bin/Debug"
},
"nativeDebugging": true
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/get_selected_objects_info.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp import get_rhino_connection, mcp, logger
@mcp.tool()
def get_selected_objects_info(ctx: Context, include_attributes: bool = False) -> str:
"""Get detailed information about the currently selected objects in Rhino
Parameters:
- include_attributes: Whether to include the custom user attributes of the objects in the response
"""
try:
rhino = get_rhino_connection()
result = rhino.send_command("get_selected_objects_info", {"include_attributes": include_attributes})
return json.dumps(result, indent=2)
except Exception as e:
logger.error(f"Error getting selected objects from Rhino: {str(e)}")
return f"Error getting selected objects: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_server/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "rhinomcp"
version = "0.1.3.6"
description = "Rhino integration through the Model Context Protocol"
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name = "Jingcheng Chen", email = "[email protected]"}
]
license = {text = "MIT"}
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dependencies = [
"fastmcp>=2.11.2",
"mcp[cli]>=1.12.4",
]
[project.scripts]
rhinomcp = "rhinomcp.server:main"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
package-dir = {"" = "src"}
[project.urls]
"Homepage" = "https://github.com/jingcheng-chen/rhinomcp"
"Bug Tracker" = "https://github.com/jingcheng-chen/rhinomcp/issues"
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/GetSelectedObjectsInfo.cs:
--------------------------------------------------------------------------------
```csharp
using Newtonsoft.Json.Linq;
using Rhino;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject GetSelectedObjectsInfo(JObject parameters)
{
var includeAttributes = parameters["include_attributes"]?.ToObject<bool>() ?? false;
var doc = RhinoDoc.ActiveDoc;
var selectedObjs = doc.Objects.GetSelectedObjects(false, false);
var result = new JArray();
foreach (var obj in selectedObjs)
{
var data = Serializer.RhinoObject(obj);
if (includeAttributes)
{
data["attributes"] = Serializer.RhinoObjectAttributes(obj);
}
result.Add(data);
}
return new JObject
{
["selected_objects"] = result
};
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/RhinoMCPServerController.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Rhino;
namespace RhinoMCPPlugin
{
class RhinoMCPServerController
{
private static RhinoMCPServer server;
public static void StartServer()
{
if (server == null)
{
server = new RhinoMCPServer();
}
server.Start();
RhinoApp.WriteLine("Server started.");
}
public static void StopServer()
{
if (server != null)
{
server.Stop();
server = null;
RhinoApp.WriteLine("Server stopped.");
}
}
public static bool IsServerRunning()
{
return server != null;
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/get_rhinoscript_python_code_guide.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
from rhinomcp import get_rhino_connection, mcp, logger, rhinoscriptsyntax_json
from typing import Any, List, Dict
@mcp.tool()
def get_rhinoscript_python_code_guide(ctx: Context, function_name: str) -> Dict[str, Any]:
"""
Return the RhinoScriptsyntax Details for a specific function.
Parameters:
- function_name: The name of the function to get the details for.
You should get the function names first by using the get_rhinoscript_python_function_names tool.
"""
try:
for module in rhinoscriptsyntax_json:
for function in module["functions"]:
if function["Name"] == function_name:
return function
return {"success": False, "message": "Function not found"}
except Exception as e:
logger.error(f"Error executing code: {str(e)}")
return {"success": False, "message": str(e)}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/DeleteObject.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using Newtonsoft.Json.Linq;
using Rhino;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject DeleteObject(JObject parameters)
{
var doc = RhinoDoc.ActiveDoc;
bool all = parameters.ContainsKey("all");
if (all)
{
doc.Objects.Clear();
doc.Views.Redraw();
return new JObject()
{
["deleted"] = true,
};
}
var obj = getObjectByIdOrName(parameters);
bool success = doc.Objects.Delete(obj.Id, true);
if (!success)
throw new InvalidOperationException($"Failed to delete object with ID {obj.Id}");
// Update views
doc.Views.Redraw();
return new JObject
{
["id"] = obj.Id,
["name"] = obj.Name,
["deleted"] = true
};
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Commands/MCPStopCommand.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;
using System.ComponentModel;
using System.Threading.Tasks;
namespace RhinoMCPPlugin.Commands
{
public class MCPStopCommand : Command
{
public MCPStopCommand()
{
// Rhino only creates one instance of each command class defined in a
// plug-in, so it is safe to store a refence in a static property.
Instance = this;
}
///<summary>The only instance of this command.</summary>
public static MCPStopCommand Instance { get; private set; }
public override string EnglishName => "mcpstop";
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
RhinoMCPServerController.StopServer();
return Result.Success;
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Commands/MCPStartCommand.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;
using System.ComponentModel;
using System.Threading.Tasks;
namespace RhinoMCPPlugin.Commands
{
public class MCPStartCommand : Command
{
public MCPStartCommand()
{
// Rhino only creates one instance of each command class defined in a
// plug-in, so it is safe to store a refence in a static property.
Instance = this;
}
///<summary>The only instance of this command.</summary>
public static MCPStartCommand Instance { get; private set; }
public override string EnglishName => "mcpstart";
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
RhinoMCPServerController.StartServer();
return Result.Success;
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/delete_object.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def delete_object(ctx: Context, id: str = None, name: str = None, all: bool = None) -> str:
"""
Delete an object from the Rhino document.
Parameters:
- id: The id of the object to delete
- name: The name of the object to delete
"""
try:
# Get the global connection
rhino = get_rhino_connection()
commandParams = {}
if id is not None:
commandParams["id"] = id
if name is not None:
commandParams["name"] = name
if all:
commandParams["all"] = all
result = rhino.send_command("delete_object", commandParams)
return f"Deleted object: {result['name']}"
except Exception as e:
logger.error(f"Error deleting object: {str(e)}")
return f"Error deleting object: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/GetOrSetCurrentLayer.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject GetOrSetCurrentLayer(JObject parameters)
{
// parse meta data
bool hasName = parameters.ContainsKey("name");
bool hasGuid = parameters.ContainsKey("guid");
string name = hasName ? castToString(parameters.SelectToken("name")) : null;
string guid = hasGuid ? castToString(parameters.SelectToken("guid")) : null;
var doc = RhinoDoc.ActiveDoc;
Layer layer = null;
if (hasName) layer = doc.Layers.FindName(name);
if (hasGuid) layer = doc.Layers.FindId(Guid.Parse(guid));
if (layer != null) doc.Layers.SetCurrentLayerIndex(layer.Index, true);
else layer = doc.Layers.CurrentLayer;
// Update views
doc.Views.Redraw();
return Serializer.SerializeLayer(layer);
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/RhinoMCPPlugin.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using Rhino;
namespace RhinoMCPPlugin
{
///<summary>
/// <para>Every RhinoCommon .rhp assembly must have one and only one PlugIn-derived
/// class. DO NOT create instances of this class yourself. It is the
/// responsibility of Rhino to create an instance of this class.</para>
/// <para>To complete plug-in information, please also see all PlugInDescription
/// attributes in AssemblyInfo.cs (you might need to click "Project" ->
/// "Show All Files" to see it in the "Solution Explorer" window).</para>
///</summary>
public class RhinoMCPPlugin : Rhino.PlugIns.PlugIn
{
public RhinoMCPPlugin()
{
Instance = this;
}
///<summary>Gets the only instance of the RhinoMCPPlugin plug-in.</summary>
public static RhinoMCPPlugin Instance { get; private set; }
// You can override methods here to change the plug-in behavior on
// loading and shut down, add options pages to the Rhino _Option command
// and maintain plug-in wide options in a document.
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
```csharp
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Rhino.PlugIns;
// Plug-in Description Attributes - all of these are optional.
// These will show in Rhino's option dialog, in the tab Plug-ins.
[assembly: PlugInDescription(DescriptionType.Address, "")]
[assembly: PlugInDescription(DescriptionType.Country, "")]
[assembly: PlugInDescription(DescriptionType.Email, "")]
[assembly: PlugInDescription(DescriptionType.Phone, "")]
[assembly: PlugInDescription(DescriptionType.Fax, "")]
[assembly: PlugInDescription(DescriptionType.Organization, "")]
[assembly: PlugInDescription(DescriptionType.UpdateUrl, "")]
[assembly: PlugInDescription(DescriptionType.WebSite, "")]
// Icons should be Windows .ico files and contain 32-bit images in the following sizes: 16, 24, 32, 48, and 256.
[assembly: PlugInDescription(DescriptionType.Icon, "rhinomcp.EmbeddedResources.plugin-utility.ico")]
// The following GUID is for the ID of the typelib if this project is exposed to COM
// This will also be the Guid of the Rhino plug-in
[assembly: Guid("f0b5b632-cc3c-43e7-bc88-da29c47b98bc")]
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Commands/MCPVersionCommand.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;
using System.ComponentModel;
using System.Threading.Tasks;
namespace RhinoMCPPlugin.Commands
{
public class MCPVersionCommand : Command
{
public MCPVersionCommand()
{
// Rhino only creates one instance of each command class defined in a
// plug-in, so it is safe to store a refence in a static property.
Instance = this;
}
///<summary>The only instance of this command.</summary>
public static MCPVersionCommand Instance { get; private set; }
public override string EnglishName => "mcpversion";
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
// get the version of the plugin from the properties of the project file
var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
Rhino.RhinoApp.WriteLine($"RhinoMCPPlugin version {version}");
return Result.Success;
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/__init__.py:
--------------------------------------------------------------------------------
```python
"""Rhino integration through the Model Context Protocol."""
__version__ = "0.1.0"
# Expose key classes and functions for easier imports
from .static.rhinoscriptsyntax import rhinoscriptsyntax_json
from .server import RhinoConnection, get_rhino_connection, mcp, logger
from .prompts.assert_general_strategy import asset_general_strategy
from .tools.create_object import create_object
from .tools.create_objects import create_objects
from .tools.delete_object import delete_object
from .tools.get_document_info import get_document_info
from .tools.get_object_info import get_object_info
from .tools.get_selected_objects_info import get_selected_objects_info
from .tools.modify_object import modify_object
from .tools.modify_objects import modify_objects
from .tools.execute_rhinoscript_python_code import execute_rhinoscript_python_code
from .tools.get_rhinoscript_python_function_names import get_rhinoscript_python_function_names
from .tools.get_rhinoscript_python_code_guide import get_rhinoscript_python_code_guide
from .tools.select_objects import select_objects
from .tools.create_layer import create_layer
from .tools.get_or_set_current_layer import get_or_set_current_layer
from .tools.delete_layer import delete_layer
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/prompts/assert_general_strategy.py:
--------------------------------------------------------------------------------
```python
from rhinomcp.server import mcp
@mcp.prompt()
def asset_general_strategy() -> str:
"""Defines the preferred strategy for creating assets in Rhino"""
return """
QUERY STRATEGY:
- if the id of the object is known, use the id to query the object.
- if the id is not known, use the name of the object to query the object.
CREATION STRATEGY:
0. Before anything, always check the document from get_document_info().
1. If the execute_rhinoscript_python_code() function is not able to create the objects, use the create_objects() function.
2. If there are multiple objects, use the method create_objects() to create multiple objects at once. Do not attempt to create them one by one if they are more than 10.
3. When including an object into document, ALWAYS make sure that the name of the object is meanful.
4. Try to include as many objects as possible accurately and efficiently. If the command is not able to include so many data, try to create the objects in batches.
When creating rhinoscript python code:
- do not hallucinate, only use the syntax that is supported by rhinoscriptsyntax or Rhino,Geometry.
- double check the code if any of the code is not correct, and fix it.
"""
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/delete_layer.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def delete_layer(
ctx: Context,
guid: str = None,
name: str = None
) -> str:
"""
Delete a layer in the Rhino document.
If name is provided, it will try to delete the layer with the given name.
If guid is provided, it will try to delete the layer with the given guid.
If neither is provided, it will return an error.
Parameters:
- name: The name of the layer to delete.
- guid: The guid of the layer to delete.
Returns:
A message indicating the layer was deleted.
Examples of params:
- name: "Layer 1"
- guid: "00000000-0000-0000-0000-000000000000"
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {}
if name is not None:
command_params["name"] = name
if guid is not None:
command_params["guid"] = guid
# Create the layer
result = rhino.send_command("delete_layer", command_params)
return result["message"]
except Exception as e:
logger.error(f"Error deleting layer: {str(e)}")
return f"Error deleting layer: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/get_rhinoscript_python_function_names.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
from rhinomcp import get_rhino_connection, mcp, logger, rhinoscriptsyntax_json
from typing import Any, List, Dict
@mcp.tool()
def get_rhinoscript_python_function_names(ctx: Context, categories: List[str]) -> List[str]:
"""
Return the RhinoScriptsyntax Function Names for specified categories.
Parameters:
- categories: A list of categories of the RhinoScriptsyntax to get.
Returns:
- A list of function names that are available in the specified categories.
The following categories are available:
- application
- block
- compat
- curve
- dimension
- document
- geometry
- grips
- group
- hatch
- layer
- light
- line
- linetype
- material
- mesh
- object
- plane
- pointvector
- selection
- surface
- toolbar
- transformation
- userdata
- userinterface
- utility
- view
"""
try:
function_names: List[str] = []
for i in rhinoscriptsyntax_json:
if i["ModuleName"] in categories:
function_names.extend([func["Name"] for func in i["functions"]])
# return the related functions
return function_names
except Exception as e:
logger.error(f"Error executing code: {str(e)}")
return []
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/create_layer.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def create_layer(
ctx: Context,
name: str = None,
color: List[int]= None,
parent: str = None,
) -> str:
"""
Create a new layer in the Rhino document.
Parameters:
- name: The name of the new layer. If omitted, Rhino automatically generates the layer name.
- color: Optional [r, g, b] color values (0-255) for the layer
- parent: Optional name of the new layer's parent layer. If omitted, the new layer will not have a parent layer.
Returns:
A message indicating the created layer name.
Examples of params:
- name: "Layer 1"
- color: [255, 0, 0]
- parent: "Default"
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {
"name": name
}
if color is not None: command_params["color"] = color
if parent is not None: command_params["parent"] = parent
# Create the layer
result = rhino.send_command("create_layer", command_params)
return f"Created layer: {result['name']}"
except Exception as e:
logger.error(f"Error creating layer: {str(e)}")
return f"Error creating layer: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/CreateLayer.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject CreateLayer(JObject parameters)
{
// parse meta data
bool hasName = parameters.ContainsKey("name");
bool hasColor = parameters.ContainsKey("color");
bool hasParent = parameters.ContainsKey("parent");
string name = hasName ? castToString(parameters.SelectToken("name")) : null;
int[] color = hasColor ? castToIntArray(parameters.SelectToken("color")) : null;
string parent = hasParent ? castToString(parameters.SelectToken("parent")) : null;
var doc = RhinoDoc.ActiveDoc;
var layer = new Layer();
if (hasName) layer.Name = name;
if (hasColor) layer.Color = Color.FromArgb(color[0], color[1], color[2]);
if (hasParent)
{
var parentLayer = doc.Layers.FindName(parent);
if (parentLayer != null)
layer.ParentLayerId = parentLayer.Id;
}
// Create a box centered at the specified point
var layerId = doc.Layers.Add(layer);
layer = doc.Layers.FindIndex(layerId);
// Update views
doc.Views.Redraw();
return Serializer.SerializeLayer(layer);
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/CreateObjects.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject CreateObjects(JObject parameters)
{
var doc = RhinoDoc.ActiveDoc;
var results = new JObject();
// Process each object in the parameters
foreach (var property in parameters.Properties())
{
try
{
// Get the object parameters
JObject objectParams = (JObject)property.Value;
// Create the object using the existing CreateObject method
JObject result = CreateObject(objectParams);
// Add the result to our results collection
results[property.Name] = result;
}
catch (Exception ex)
{
// If there's an error creating this object, add the error to the results
results[property.Name] = new JObject
{
["error"] = ex.Message
};
}
}
// Update views
doc.Views.Redraw();
return results;
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/ModifyObjects.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Drawing;
using System.Linq;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject ModifyObjects(JObject parameters)
{
bool all = parameters.ContainsKey("all");
JArray objectParameters = (JArray)parameters["objects"];
var doc = RhinoDoc.ActiveDoc;
var objects = doc.Objects.ToList();
if (all && objectParameters.Count == 1)
{
// Get the first modification parameters (excluding the "all" property)
JObject firstModification = (JObject)objectParameters.FirstOrDefault()!;
// Create new parameters object with all object IDs
foreach (var obj in objects)
{
// Create a new copy of the modification parameters for each object
JObject newModification = new JObject(firstModification) { ["id"] = obj.Id.ToString() };
objectParameters.Add(newModification);
}
}
var i = 0;
foreach (JObject parameter in objectParameters)
{
if (parameter.ContainsKey("id"))
{
ModifyObject(parameter);
i++;
}
}
doc.Views.Redraw();
return new JObject() { ["modified"] = i };
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/DeleteLayer.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject DeleteLayer(JObject parameters)
{
// parse meta data
bool hasName = parameters.ContainsKey("name");
bool hasGuid = parameters.ContainsKey("guid");
string name = hasName ? castToString(parameters.SelectToken("name")) : null;
string guid = hasGuid ? castToString(parameters.SelectToken("guid")) : null;
var doc = RhinoDoc.ActiveDoc;
Layer layer = null;
if (hasName) layer = doc.Layers.FindName(name);
if (hasGuid) layer = doc.Layers.FindId(Guid.Parse(guid));
if (layer == null)
{
return new JObject
{
["success"] = false,
["message"] = "Layer not found"
};
}
if (layer == null)
{
return new JObject
{
["success"] = false,
["message"] = "Layer not found"
};
}
name = layer.Name;
doc.Layers.Delete(layer.Index, true);
// Update views
doc.Views.Redraw();
return new JObject
{
["success"] = true,
["message"] = $"Layer {name} deleted"
};
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/get_or_set_current_layer.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def get_or_set_current_layer(
ctx: Context,
guid: str = None,
name: str = None
) -> str:
"""
Get or set the current layer in the Rhino document.
If name is provided, it will try to set the current layer to the layer with the given name.
If guid is provided, it will try to set the current layer to the layer with the given guid.
If neither is provided, it will return the current layer.
Parameters:
- name: The name of the layer to set the current layer to.
- guid: The guid of the layer to set the current layer to.
Returns:
A message indicating the current layer.
Examples of params:
- name: "Layer 1"
- guid: "00000000-0000-0000-0000-000000000000"
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {}
if name is not None:
command_params["name"] = name
if guid is not None:
command_params["guid"] = guid
# Create the layer
result = rhino.send_command("get_or_set_current_layer", command_params)
return f"Current layer: {result['name']}"
except Exception as e:
logger.error(f"Error getting or setting current layer: {str(e)}")
return f"Error getting or setting current layer: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/modify_objects.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def modify_objects(
ctx: Context,
objects: List[Dict[str, Any]],
all: bool = None
) -> str:
"""
Create multiple objects at once in the Rhino document.
Parameters:
- objects: A List of objects, each containing the parameters for a single object modification
- all: Optional boolean to modify all objects, if true, only one object is required in the objects dictionary
Each object can have the following parameters:
- id: The id of the object to modify
- new_color: Optional [r, g, b] color values (0-255) for the object
- translation: Optional [x, y, z] translation vector
- rotation: Optional [x, y, z] rotation in radians
- scale: Optional [x, y, z] scale factors
- visible: Optional boolean to set visibility
Returns:
A message indicating the modified objects.
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {}
command_params["objects"] = objects
if all:
command_params["all"] = all
result = rhino.send_command("modify_objects", command_params)
return f"Modified {result['modified']} objects"
except Exception as e:
logger.error(f"Error modifying objects: {str(e)}")
return f"Error modifying objects: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/get_object_info.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp import get_rhino_connection, mcp, logger
from typing import Dict, Any
@mcp.tool()
def get_object_info(ctx: Context, id: str = None, name: str = None) -> Dict[str, Any]:
"""
Get detailed information about a specific object in the Rhino document.
The information contains the object's id, name, type, all custom user attributes and geometry info.
You can either provide the id or the object_name of the object to get information about.
If both are provided, the id will be used.
Returns:
- A dictionary containing the object's information
- The dictionary will have the following keys:
- "id": The id of the object
- "name": The name of the object
- "type": The type of the object
- "layer": The layer of the object
- "material": The material of the object
- "color": The color of the object
- "bounding_box": The bounding box of the object
- "geometry": The geometry info of the object
- "attributes": A dictionary containing all custom user attributes of the object
Parameters:
- id: The id of the object to get information about
- name: The name of the object to get information about
"""
try:
rhino = get_rhino_connection()
return rhino.send_command("get_object_info", {"id": id, "name": name})
except Exception as e:
logger.error(f"Error getting object info from Rhino: {str(e)}")
return {
"error": str(e)
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/execute_rhinoscript_python_code.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def execute_rhinoscript_python_code(ctx: Context, code: str) -> Dict[str, Any]:
"""
Execute arbitrary RhinoScript code in Rhino.
Parameters:
- code: The RhinoScript code to execute
GUIDE:
1. To get any output from the script, you should use the python `print` function.
2. You can get a list of all possible functions names that can be used by using the get_rhinoscript_python_function_names tool.
3. You can get the details of a specific function by using the get_rhinoscript_python_code_guide tool.
Example:
- Your task is: "Create a loft surface between two curves."
- get_rhinoscript_python_function_names(["surface", "curve"])
- This will return the function names that are necessary for creating the code.
- get_rhinoscript_python_code_guide("AddLoftSrf")
- This will return the syntax of the code that are necessary for creating the code.
Any changes made to the document will be undone if the script returns failure.
DO NOT HALLUCINATE, ONLY USE THE SYNTAX THAT IS SUPPORTED BY RHINO.GEOMETRY OR RHINOSCRIPT.
"""
try:
# Get the global connection
rhino = get_rhino_connection()
return rhino.send_command("execute_rhinoscript_python_code", {"code": code})
except Exception as e:
logger.error(f"Error executing code: {str(e)}")
return {"success": False, "message": str(e)}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/ExecuteRhinoscript.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
using Rhino.Runtime;
using System.Text;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject ExecuteRhinoscript(JObject parameters)
{
var doc = RhinoDoc.ActiveDoc;
string code = parameters["code"]?.ToString();
if (string.IsNullOrEmpty(code))
{
throw new Exception("Code is required");
}
// register undo
var undoRecordSerialNumber = doc.BeginUndoRecord("ExecuteRhinoScript");
JObject result = new JObject();
try
{
var output = new StringBuilder();
// Create a new Python script instance
PythonScript pythonScript = PythonScript.Create();
pythonScript.Output += (message) =>
{
output.Append(message);
};
// Setup the script context with the current document
if (doc != null)
pythonScript.SetupScriptContext(doc);
// Execute the Python code
pythonScript.ExecuteScript(code);
result["success"] = true;
result["result"] = $"Script successfully executed! Print output: {output}";
}
catch (Exception ex)
{
result["success"] = false;
result["message"] = $"Error executing rhinoscript: {ex}";
}
finally
{
// undo
doc.EndUndoRecord(undoRecordSerialNumber);
}
// if the script failed, undo the changes
if (!result["success"].ToObject<bool>())
{
doc.Undo();
}
return result;
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/modify_object.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def modify_object(
ctx: Context,
id: str = None,
name: str = None,
new_name: str = None,
new_color: List[int] = None,
translation: List[float] = None,
rotation: List[float] = None,
scale: List[float] = None,
visible: bool = None
) -> str:
"""
Modify an existing object in the Rhino document.
Parameters:
- id: The id of the object to modify
- name: The name of the object to modify
- new_name: Optional new name for the object
- new_color: Optional [r, g, b] color values (0-255) for the object
- translation: Optional [x, y, z] translation vector
- rotation: Optional [x, y, z] rotation in radians
- scale: Optional [x, y, z] scale factors
- visible: Optional boolean to set visibility
"""
try:
# Get the global connection
rhino = get_rhino_connection()
params : Dict[str, Any] = {}
if id is not None:
params["id"] = id
if name is not None:
params["name"] = name
if new_name is not None:
params["new_name"] = new_name
if new_color is not None:
params["new_color"] = new_color
if translation is not None:
params["translation"] = translation
if rotation is not None:
params["rotation"] = rotation
if scale is not None:
params["scale"] = scale
if visible is not None:
params["visible"] = visible
result = rhino.send_command("modify_object", params)
return f"Modified object: {result['name']}"
except Exception as e:
logger.error(f"Error modifying object: {str(e)}")
return f"Error modifying object: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/select_objects.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def select_objects(
ctx: Context,
filters: Dict[str, List[Any]] = {},
filters_type: str = "and",
) -> str:
"""
Select objects in the Rhino document.
Parameters:
- filters: A dictionary containing the filters. The filters parameter is necessary, unless it's empty, in which case all objects will be selected.
- filters_type: The type of the filters, it's "and" or "or", default is "and"
Note:
The filter value is always a list, even if it's a single value. The reason is that a filter can contain multiple values, for example when we query by a attribute that has EITHER value1 OR value2.
The filters dictionary can contain the following keys:
- name: The name of the object
- color: The color of the object, for example [255, 0, 0]
Additionaly, rhino allows to have user custom attributes, which can be used to filters the objects.
For example, if the object has a user custom attribute called "category", the filters dictionary can contain:
- category: custom_attribute_value
Example:
filters = {
"name": ["object_name1", "object_name2"],
"category": ["custom_attribute_value"]
},
filters_type = "or"
Returns:
A number indicating the number of objects that have been selected.
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {
"filters": filters,
"filters_type": filters_type
}
result = rhino.send_command("select_objects", command_params)
return f"Selected {result['count']} objects"
except Exception as e:
logger.error(f"Error selecting objects: {str(e)}")
return f"Error selecting objects: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/GetDocumentInfo.cs:
--------------------------------------------------------------------------------
```csharp
using Newtonsoft.Json.Linq;
using Rhino;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject GetDocumentInfo(JObject parameters)
{
const int LIMIT = 30;
RhinoApp.WriteLine("Getting document info...");
var doc = RhinoDoc.ActiveDoc;
var metaData = new JObject
{
["name"] = doc.Name,
["date_created"] = doc.DateCreated,
["date_modified"] = doc.DateLastEdited,
["tolerance"] = doc.ModelAbsoluteTolerance,
["angle_tolerance"] = doc.ModelAngleToleranceDegrees,
["path"] = doc.Path,
["units"] = doc.ModelUnitSystem.ToString(),
};
var objectData = new JArray();
// Collect minimal object information (limit to first 10 objects)
int count = 0;
foreach (var docObject in doc.Objects)
{
if (count >= LIMIT) break;
objectData.Add(Serializer.RhinoObject(docObject));
count++;
}
var layerData = new JArray();
count = 0;
foreach (var docLayer in doc.Layers)
{
if (count >= LIMIT) break;
layerData.Add(new JObject
{
["id"] = docLayer.Id.ToString(),
["name"] = docLayer.Name,
["color"] = docLayer.Color.ToString(),
["visible"] = docLayer.IsVisible,
["locked"] = docLayer.IsLocked
});
count++;
}
var result = new JObject
{
["meta_data"] = metaData,
["object_count"] = doc.Objects.Count,
["objects"] = objectData,
["layer_count"] = doc.Layers.Count,
["layers"] = layerData
};
RhinoApp.WriteLine($"Document info collected: {count} objects");
return result;
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/mcp-server-publish.yml:
--------------------------------------------------------------------------------
```yaml
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
release-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Build release distributions
run: |
cd rhino_mcp_server
python -m pip install build
python -m build
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: rhino_mcp_server/dist/
pypi-publish:
runs-on: ubuntu-latest
needs:
- release-build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Dedicated environments with protections for publishing are strongly recommended.
# For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
environment:
name: pypi
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
url: https://pypi.org/p/rhinomcp
#
# ALTERNATIVE: if your GitHub Release name is the PyPI project version string
# ALTERNATIVE: exactly, uncomment the following line instead:
# url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: rhino_mcp_server/dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
packages-dir: rhino_mcp_server/dist/
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/ModifyObject.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject ModifyObject(JObject parameters)
{
var doc = RhinoDoc.ActiveDoc;
var obj = getObjectByIdOrName(parameters);
var geometry = obj.Geometry;
var xform = Transform.Identity;
// Handle different modifications based on parameters
bool attributesModified = false;
bool geometryModified = false;
// Change name if provided
if (parameters["new_name"] != null)
{
string name = parameters["new_name"].ToString();
obj.Attributes.Name = name;
attributesModified = true;
}
// Change color if provided
if (parameters["new_color"] != null)
{
int[] color = parameters["new_color"]?.ToObject<int[]>() ?? new[] { 0, 0, 0 };
obj.Attributes.ObjectColor = Color.FromArgb(color[0], color[1], color[2]);
obj.Attributes.ColorSource = ObjectColorSource.ColorFromObject;
attributesModified = true;
}
// Change translation if provided
if (parameters["translation"] != null)
{
xform *= applyTranslation(parameters);
geometryModified = true;
}
// Apply scale if provided
if (parameters["scale"] != null)
{
xform *= applyScale(parameters, geometry);
geometryModified = true;
}
// Apply rotation if provided
if (parameters["rotation"] != null)
{
xform *= applyRotation(parameters, geometry);
geometryModified = true;
}
if (attributesModified)
{
// Update the object attributes if needed
doc.Objects.ModifyAttributes(obj, obj.Attributes, true);
}
if (geometryModified)
{
// Update the object geometry if needed
doc.Objects.Transform(obj, xform, true);
}
// Update views
doc.Views.Redraw();
return Serializer.RhinoObject(getObjectByIdOrName(new JObject { ["id"] = obj.Id }));
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/create_objects.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def create_objects(
ctx: Context,
objects: List[Dict[str, Any]]
) -> str:
"""
Create multiple objects at once in the Rhino document.
Parameters:
- objects: A list of dictionaries, each containing the parameters for a single object
Each object should have the following values:
- type: Object type ("POINT", "LINE", "POLYLINE", "BOX", "SPHERE", etc.)
- name: Optional name for the object
- color: Optional [r, g, b] color values (0-255) for the object
- params: Type-specific parameters dictionary (see documentation for each type in create_object() function)
- translation: Optional [x, y, z] translation vector
- rotation: Optional [x, y, z] rotation in radians
- scale: Optional [x, y, z] scale factors
Returns:
A message indicating the created objects.
Examples of params:
[
{
"type": "POINT",
"name": "Point 1",
"params": {"x": 0, "y": 0, "z": 0}
},
{
"type": "LINE",
"name": "Line 1",
"params": {"start": [0, 0, 0], "end": [1, 1, 1]}
},
{
"type": "POLYLINE",
"name": "Polyline 1",
"params": {"points": [[0, 0, 0], [1, 1, 1], [2, 2, 2]]}
},
{
"type": "CURVE",
"name": "Curve 1",
"params": {"points": [[0, 0, 0], [1, 1, 1], [2, 2, 2]], "degree": 3}
},
{
"type": "BOX",
"name": "Box 1",
"color": [255, 0, 0],
"params": {"width": 1.0, "length": 1.0, "height": 1.0},
"translation": [0, 0, 0],
"rotation": [0, 0, 0],
"scale": [1, 1, 1]
},
{
"type": "SPHERE",
"name": "Sphere 1",
"color": [0, 255, 0],
"params": {"radius": 1.0},
"translation": [0, 0, 0],
"rotation": [0, 0, 0],
"scale": [1, 1, 1]
}
]
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {}
for obj in objects:
command_params[obj["name"]] = obj
result = rhino.send_command("create_objects", command_params)
return f"Created {len(result)} objects"
except Exception as e:
logger.error(f"Error creating object: {str(e)}")
return f"Error creating object: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/linetype.py:
--------------------------------------------------------------------------------
```python
import Rhino
import scriptcontext
import rhinocompat as compat
from rhinoscript import utility as rhutil
def __getlinetype(name_or_id):
id = rhutil.coerceguid(name_or_id)
if id: return scriptcontext.doc.Linetypes.FindId(id)
return scriptcontext.doc.Linetypes.FindName(name_or_id)
def IsLinetype(name_or_id):
"""Verifies the existance of a linetype in the document
Parameters:
name_or_id (guid|str): The name or identifier of an existing linetype.
Returns:
bool: True or False
Example:
import rhinoscriptsyntax as rs
name = rs.GetString("Linetype name")
if rs.IsLinetype(name): print("The linetype exists.")
else: print("The linetype does not exist")
See Also:
IsLinetypeReference
"""
lt = __getlinetype(name_or_id)
return lt is not None
def IsLinetypeReference(name_or_id):
"""Verifies that an existing linetype is from a reference file
Parameters:
name_or_id (guid|str): The name or identifier of an existing linetype.
Returns:
bool: True or False
Example:
import rhinoscriptsyntax as rs
name = rs.GetString("Linetype name")
if rs.IsLinetype(name):
if rs.IsLinetypeReference(name):
print("The linetype is a reference linetype.")
else:
print("The linetype is not a reference linetype.")
else:
print("The linetype does not exist.")
See Also:
IsLinetype
"""
lt = __getlinetype(name_or_id)
if lt is None: raise ValueError("unable to coerce %s into linetype"%name_or_id)
return lt.IsReference
def LinetypeCount():
"""Returns number of linetypes in the document
Returns:
number: the number of linetypes in the document
Example:
import rhinoscriptsyntax as rs
count = rs.LinetypeCount()
print("There are {} linetypes".format(count))
See Also:
LinetypeNames
"""
return scriptcontext.doc.Linetypes.Count
def LinetypeNames(sort=False):
"""Returns names of all linetypes in the document
Parameters:
sort (bool, optional): return a sorted list of the linetype names
Returns:
list(str, ...): list of linetype names if successful
Example:
import rhinoscriptsyntax as rs
names = rs.LinetypeNames()
if names:
for name in names: print(name)
See Also:
LinetypeCount
"""
count = scriptcontext.doc.Linetypes.Count
rc = []
for i in compat.RANGE(count):
linetype = scriptcontext.doc.Linetypes[i]
if not linetype.IsDeleted: rc.append(linetype.Name)
if sort: rc.sort()
return rc
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/SelectObjects.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Drawing;
using System.Linq;
using Newtonsoft.Json.Linq;
using Rhino;
using System.Collections.Generic;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject SelectObjects(JObject parameters)
{
JObject filters = (JObject)parameters["filters"];
var doc = RhinoDoc.ActiveDoc;
var objects = doc.Objects.ToList();
var selectedObjects = new List<Guid>();
var filtersType = (string)parameters["filters_type"];
var hasName = false;
var hasColor = false;
var customAttributes = new Dictionary<string, List<string>>();
// no filter means all are selected
if (filters.Count == 0)
{
doc.Objects.UnselectAll();
doc.Objects.Select(objects.Select(o => o.Id));
doc.Views.Redraw();
return new JObject() { ["count"] = objects.Count };
}
foreach (JProperty f in filters.Properties())
{
if (f.Name == "name") hasName = true;
if (f.Name == "color") hasColor = true;
if (f.Name != "name" && f.Name != "color") customAttributes.Add(f.Name, castToStringList(f.Value));
}
var name = hasName ? castToString(filters.SelectToken("name")) : null;
var color = hasColor ? castToIntArray(filters.SelectToken("color")) : null;
if (filtersType == "and")
foreach (var obj in objects)
{
var attributeMatch = true;
if (hasName && obj.Name != name) continue;
if (hasColor && obj.Attributes.ObjectColor.R != color[0] && obj.Attributes.ObjectColor.G != color[1] && obj.Attributes.ObjectColor.B != color[2]) continue;
foreach (var customAttribute in customAttributes)
{
foreach (var value in customAttribute.Value)
{
if (obj.Attributes.GetUserString(customAttribute.Key) != value) attributeMatch = false;
}
}
if (!attributeMatch) continue;
selectedObjects.Add(obj.Id);
}
else if (filtersType == "or")
foreach (var obj in objects)
{
var attributeMatch = false;
if (hasName && obj.Name == name) attributeMatch = true;
if (hasColor && obj.Attributes.ObjectColor.R == color[0] && obj.Attributes.ObjectColor.G == color[1] && obj.Attributes.ObjectColor.B == color[2]) attributeMatch = true;
foreach (var customAttribute in customAttributes)
{
foreach (var value in customAttribute.Value)
{
if (obj.Attributes.GetUserString(customAttribute.Key) == value) attributeMatch = true;
}
}
if (!attributeMatch) continue;
selectedObjects.Add(obj.Id);
}
doc.Objects.UnselectAll();
doc.Objects.Select(selectedObjects);
doc.Views.Redraw();
return new JObject() { ["count"] = selectedObjects.Count };
}
}
```
--------------------------------------------------------------------------------
/.github/workflows/rhino-plugin-publish.yml:
--------------------------------------------------------------------------------
```yaml
name: Build and Publish Rhino Plugin
on:
release:
types: [published]
jobs:
release-build:
runs-on: ubuntu-latest
env:
SOLUTION_PATH: ${{ github.workspace }}/rhino_mcp_plugin/rhinomcp.sln
PROJECT_PATH: ${{ github.workspace }}/rhino_mcp_plugin/rhinomcp.csproj
ARTIFACT_NAME: rhinomcp
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '7.0.x'
- name: Restore NuGet packages
run: |
dotnet restore ${{ env.SOLUTION_PATH }}
- name: Build solution with MSBuild
run: |
dotnet msbuild ${{ env.SOLUTION_PATH }} /p:Configuration=Release /p:Platform="Any CPU"
- name: Prepare artifacts
run: |
mkdir -p dist/net7.0
# Copy DLL, RHP, and any other necessary files to the dist folder
cp ${{ github.workspace }}/rhino_mcp_plugin/bin/Release/net7.0/*.dll dist/net7.0/
cp ${{ github.workspace }}/rhino_mcp_plugin/bin/Release/net7.0/*.rhp dist/net7.0/
cp ${{ github.workspace }}/rhino_mcp_plugin/manifest.yml dist/
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: dist/
publish:
name: Publish to Yak and GitHub Release
runs-on: ubuntu-latest
needs:
- release-build
env:
ARTIFACT_NAME: rhinomcp
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.ARTIFACT_NAME }}
path: dist
- name: Setup Yak
run: |
# Create a directory for yak
mkdir -p ${{ github.workspace }}/yakfolder
# Download Linux version of yak
curl -L "https://files.mcneel.com/yak/tools/0.13.0/linux-x64/yak" -o ${{ github.workspace }}/yakfolder/yak
# Make it executable
chmod +x ${{ github.workspace }}/yakfolder/yak
# Add to path and verify
echo "${{ github.workspace }}/yakfolder" >> $GITHUB_PATH
echo "PATH is now: $PATH:${{ github.workspace }}/yakfolder"
- name: Pack and Push to Yak
run: |
cd dist
export YAK_TOKEN=${{ secrets.YAK_API_KEY }}
# Build yak package
yak build
# List files to verify the .yak file was created
ls -la
# Find the .yak package
# Use -maxdepth 1 to avoid finding .yak files in subdirectories if any
yakPackageFile=$(find . -maxdepth 1 -name "*.yak" -type f | head -1)
if [ -z "$yakPackageFile" ]; then
echo "Error: No .yak package was created in the 'dist' directory."
exit 1
fi
echo "Found package: $yakPackageFile"
# Get just the filename for the release asset name and GITHUB_ENV
yakPackageBasename=$(basename "$yakPackageFile")
echo "YAK_PACKAGE_BASENAME=$yakPackageBasename" >> $GITHUB_ENV
echo "YAK_PACKAGE_PATH_IN_DIST=$yakPackageFile" >> $GITHUB_ENV # Will be e.g., ./rhinomcp-1.2.3.yak
# Push to yak server
yak push "$yakPackageFile"
- name: Upload .yak package to GitHub Release
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dist/${{ env.YAK_PACKAGE_PATH_IN_DIST }} # Path relative to GITHUB_WORKSPACE
asset_name: ${{ env.YAK_PACKAGE_BASENAME }}
asset_content_type: application/zip
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Serializers/Serializer.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
namespace rhinomcp.Serializers
{
public static class Serializer
{
public static RhinoDoc doc = RhinoDoc.ActiveDoc;
public static JObject SerializeColor(Color color)
{
return new JObject()
{
["r"] = color.R,
["g"] = color.G,
["b"] = color.B
};
}
public static JArray SerializePoint(Point3d pt)
{
return new JArray
{
Math.Round(pt.X, 2),
Math.Round(pt.Y, 2),
Math.Round(pt.Z, 2)
};
}
public static JArray SerializePoints(IEnumerable<Point3d> pts)
{
return new JArray
{
pts.Select(p => SerializePoint(p))
};
}
public static JObject SerializeCurve(Curve crv)
{
return new JObject
{
["type"] = "Curve",
["geometry"] = new JObject
{
["points"] = SerializePoints(crv.ControlPolygon().ToArray()),
["degree"] = crv.Degree.ToString()
}
};
}
public static JArray SerializeBBox(BoundingBox bbox)
{
return new JArray
{
new JArray { bbox.Min.X, bbox.Min.Y, bbox.Min.Z },
new JArray { bbox.Max.X, bbox.Max.Y, bbox.Max.Z }
};
}
public static JObject SerializeLayer(Layer layer)
{
return new JObject
{
["id"] = layer.Id.ToString(),
["name"] = layer.Name,
["color"] = SerializeColor(layer.Color),
["parent"] = layer.ParentLayerId.ToString()
};
}
public static JObject RhinoObjectAttributes(RhinoObject obj)
{
var attributes = obj.Attributes.GetUserStrings();
var attributesDict = new JObject();
foreach (string key in attributes.AllKeys)
{
attributesDict[key] = attributes[key];
}
return attributesDict;
}
public static JObject RhinoObject(RhinoObject obj)
{
var objInfo = new JObject
{
["id"] = obj.Id.ToString(),
["name"] = obj.Name ?? "(unnamed)",
["type"] = obj.ObjectType.ToString(),
["layer"] = doc.Layers[obj.Attributes.LayerIndex].Name,
["material"] = obj.Attributes.MaterialIndex.ToString(),
["color"] = SerializeColor(obj.Attributes.ObjectColor)
};
// add boundingbox
BoundingBox bbox = obj.Geometry.GetBoundingBox(true);
objInfo["bounding_box"] = SerializeBBox(bbox);
// Add geometry data
if (obj.Geometry is Rhino.Geometry.Point point)
{
objInfo["type"] = "POINT";
objInfo["geometry"] = SerializePoint(point.Location);
}
else if (obj.Geometry is Rhino.Geometry.LineCurve line)
{
objInfo["type"] = "LINE";
objInfo["geometry"] = new JObject
{
["start"] = SerializePoint(line.Line.From),
["end"] = SerializePoint(line.Line.To)
};
}
else if (obj.Geometry is Rhino.Geometry.PolylineCurve polyline)
{
objInfo["type"] = "POLYLINE";
objInfo["geometry"] = new JObject
{
["points"] = SerializePoints(polyline.ToArray())
};
}
else if (obj.Geometry is Rhino.Geometry.Curve curve)
{
var crv = SerializeCurve(curve);
objInfo["type"] = crv["type"];
objInfo["geometry"] = crv["geometry"];
}
else if (obj.Geometry is Rhino.Geometry.Extrusion extrusion)
{
objInfo["type"] = "EXTRUSION";
}
return objInfo;
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/_utils.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
private double castToDouble(JToken token)
{
return token?.ToObject<double>() ?? 0;
}
private double[] castToDoubleArray(JToken token)
{
return token?.ToObject<double[]>() ?? new double[] { 0, 0, 0 };
}
private double[][] castToDoubleArray2D(JToken token)
{
List<double[]> result = new List<double[]>();
foreach (var t in (JArray)token)
{
double[] inner = castToDoubleArray(t);
result.Add(inner);
}
return result.ToArray();
}
private int castToInt(JToken token)
{
return token?.ToObject<int>() ?? 0;
}
private int[] castToIntArray(JToken token)
{
return token?.ToObject<int[]>() ?? new int[] { 0, 0, 0 };
}
private bool[] castToBoolArray(JToken token)
{
return token?.ToObject<bool[]>() ?? new bool[] { false, false };
}
private List<string> castToStringList(JToken token)
{
return token?.ToObject<List<string>>() ?? new List<string>();
}
private bool castToBool(JToken token)
{
return token?.ToObject<bool>() ?? false;
}
private string castToString(JToken token)
{
return token?.ToString();
}
private Guid castToGuid(JToken token)
{
var guid = token?.ToString();
if (guid == null) return Guid.Empty;
return new Guid(guid);
}
private List<Point3d> castToPoint3dList(JToken token)
{
double[][] points = castToDoubleArray2D(token);
var ptList = new List<Point3d>();
foreach (var point in points)
{
ptList.Add(new Point3d(point[0], point[1], point[2]));
}
return ptList;
}
private Point3d castToPoint3d(JToken token)
{
double[] point = castToDoubleArray(token);
return new Point3d(point[0], point[1], point[2]);
}
private RhinoObject getObjectByIdOrName(JObject parameters)
{
string objectId = parameters["id"]?.ToString();
string objectName = parameters["name"]?.ToString();
var doc = RhinoDoc.ActiveDoc;
RhinoObject obj = null;
if (!string.IsNullOrEmpty(objectId))
obj = doc.Objects.Find(new Guid(objectId));
else if (!string.IsNullOrEmpty(objectName))
{
// we assume there's only one of the object with the given name
var objs = doc.Objects.GetObjectList(new ObjectEnumeratorSettings() { NameFilter = objectName }).ToList();
if (objs == null) throw new InvalidOperationException($"Object with name {objectName} not found.");
if (objs.Count > 1) throw new InvalidOperationException($"Multiple objects with name {objectName} found.");
obj = objs[0];
}
if (obj == null)
throw new InvalidOperationException($"Object with ID {objectId} not found");
return obj;
}
private Transform applyRotation(JObject parameters, GeometryBase geometry)
{
double[] rotation = parameters["rotation"].ToObject<double[]>();
var xform = Transform.Identity;
// Calculate the center for rotation
BoundingBox bbox = geometry.GetBoundingBox(true);
Point3d center = bbox.Center;
// Create rotation transformations (in radians)
Transform rotX = Transform.Rotation(rotation[0], Vector3d.XAxis, center);
Transform rotY = Transform.Rotation(rotation[1], Vector3d.YAxis, center);
Transform rotZ = Transform.Rotation(rotation[2], Vector3d.ZAxis, center);
// Apply transformations
xform *= rotX;
xform *= rotY;
xform *= rotZ;
return xform;
}
private Transform applyTranslation(JObject parameters)
{
double[] translation = parameters["translation"].ToObject<double[]>();
var xform = Transform.Identity;
Vector3d move = new Vector3d(translation[0], translation[1], translation[2]);
xform *= Transform.Translation(move);
return xform;
}
private Transform applyScale(JObject parameters, GeometryBase geometry)
{
double[] scale = parameters["scale"].ToObject<double[]>();
var xform = Transform.Identity;
// Calculate the min for scaling
BoundingBox bbox = geometry.GetBoundingBox(true);
Point3d anchor = bbox.Min;
Plane plane = Plane.WorldXY;
plane.Origin = anchor;
// Create scale transformation
Transform scaleTransform = Transform.Scale(plane, scale[0], scale[1], scale[2]);
xform *= scaleTransform;
return xform;
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/tools/create_object.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import Context
import json
from rhinomcp.server import get_rhino_connection, mcp, logger
from typing import Any, List, Dict
@mcp.tool()
def create_object(
ctx: Context,
type: str = "BOX",
name: str = None,
color: List[int]= None,
params: Dict[str, Any] = {},
translation: List[float]= None,
rotation: List[float]= None,
scale: List[float]= None,
) -> str:
"""
Create a new object in the Rhino document.
Parameters:
- type: Object type ("POINT", "LINE", "POLYLINE", "CIRCLE", "ARC", "ELLIPSE", "CURVE", "BOX", "SPHERE", "CONE", "CYLINDER", "PIPE", "SURFACE")
- name: Optional name for the object
- color: Optional [r, g, b] color values (0-255) for the object
- params: Type-specific parameters dictionary (see documentation for each type)
- translation: Optional [x, y, z] translation vector
- rotation: Optional [x, y, z] rotation in radians
- scale: Optional [x, y, z] scale factors
The params dictionary is type-specific.
For POINT, the params dictionary should contain the following keys:
- x: x coordinate of the point
- y: y coordinate of the point
- z: z coordinate of the point
For LINE, the params dictionary should contain the following keys:
- start: [x, y, z] start point of the line
- end: [x, y, z] end point of the line
For POLYLINE, the params dictionary should contain the following keys:
- points: List of [x, y, z] points that define the polyline
For CIRCLE, the params dictionary should contain the following keys:
- center: [x, y, z] center point of the circle
- radius: Radius of the circle
For ARC, the params dictionary should contain the following keys:
- center: [x, y, z] center point of the arc
- radius: Radius of the arc
- angle: Angle of the arc in degrees
For ELLIPSE, the params dictionary should contain the following keys:
- center: [x, y, z] center point of the ellipse
- radius_x: Radius of the ellipse along X axis
- radius_y: Radius of the ellipse along Y axis
For CURVE, the params dictionary should contain the following keys:
- points: List of [x, y, z] control points that define the curve
- degree: Degree of the curve (default is 3, if user asked for smoother curve, degree can be higher)
If the curve is closed, the first and last points should be the same.
For BOX, the params dictionary should contain the following keys:
- width: Width of the box along X axis of the object
- length: Length of the box along Y axis of the object
- height: Height of the box along Z axis of the object
For SPHERE, the params dictionary should contain the following key:
- radius: Radius of the sphere
For CONE, the params dictionary should contain the following keys:
- radius: Radius of the cone
- height: Height of the cone
- cap: Boolean to indicate if the cone should be capped at the base, default is True
For CYLINDER, the params dictionary should contain the following keys:
- radius: Radius of the cylinder
- height: Height of the cylinder
- cap: Boolean to indicate if the cylinder should be capped at the base, default is True
For SURFACE, the params dictionary should contain the following keys:
- count : ([number, number]) Tuple of two numbers defining number of points in the u,v directions
- points: List of [x, y, z] points that define the surface
- degree: ([number, number], optional) Degree of the surface (default is 3, if user asked for smoother surface, degree can be higher)
- closed: ([bool, bool], optional) Two booleans defining if the surface is closed in the u,v directions
Returns:
A message indicating the created object name.
Examples of params:
- POINT: {"x": 0, "y": 0, "z": 0}
- LINE: {"start": [0, 0, 0], "end": [1, 1, 1]}
- POLYLINE: {"points": [[0, 0, 0], [1, 1, 1], [2, 2, 2]]}
- CIRCLE: {"center": [0, 0, 0], "radius": 1.0}
- CURVE: {"points": [[0, 0, 0], [1, 1, 1], [2, 2, 2]], "degree": 3}
- BOX: {"width": 1.0, "length": 1.0, "height": 1.0}
- SPHERE: {"radius": 1.0}
- CONE: {"radius": 1.0, "height": 1.0, "cap": True}
- CYLINDER: {"radius": 1.0, "height": 1.0, "cap": True}
- SURFACE: {"count": (3, 3), "points": [[0, 0, 0], [1, 0, 0], [2, 0, 0], [0, 1, 0], [1, 1, 0], [2, 1, 0], [0, 2, 0], [1, 2, 0], [2, 2, 0]], "degree": (3, 3), "closed": (False, False)}
"""
try:
# Get the global connection
rhino = get_rhino_connection()
command_params = {
"type": type,
"params": params
}
if translation is not None: command_params["translation"] = translation
if rotation is not None: command_params["rotation"] = rotation
if scale is not None: command_params["scale"] = scale
if name: command_params["name"] = name
if color: command_params["color"] = color
# Create the object
result = result = rhino.send_command("create_object", command_params)
return f"Created {type} object: {result['name']}"
except Exception as e:
logger.error(f"Error creating object: {str(e)}")
return f"Error creating object: {str(e)}"
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/Functions/CreateObject.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Drawing;
using Newtonsoft.Json.Linq;
using Rhino;
using Rhino.DocObjects;
using Rhino.Geometry;
using rhinomcp.Serializers;
namespace RhinoMCPPlugin.Functions;
public partial class RhinoMCPFunctions
{
public JObject CreateObject(JObject parameters)
{
// parse meta data
string type = castToString(parameters.SelectToken("type"));
string name = castToString(parameters.SelectToken("name"));
bool customColor = parameters.ContainsKey("color");
int[] color = castToIntArray(parameters.SelectToken("color"));
JObject geoParams = (JObject)parameters.SelectToken("params");
var doc = RhinoDoc.ActiveDoc;
Guid objectId = Guid.Empty;
// Create a box centered at the specified point
switch (type)
{
case "POINT":
double x = castToDouble(geoParams.SelectToken("x"));
double y = castToDouble(geoParams.SelectToken("y"));
double z = castToDouble(geoParams.SelectToken("z"));
objectId = doc.Objects.AddPoint(x, y, z);
break;
case "LINE":
double[] start = castToDoubleArray(geoParams.SelectToken("start"));
double[] end = castToDoubleArray(geoParams.SelectToken("end"));
var ptStart = new Point3d(start[0], start[1], start[2]);
var ptEnd = new Point3d(end[0], end[1], end[2]);
objectId = doc.Objects.AddLine(ptStart, ptEnd);
break;
case "POLYLINE":
List<Point3d> ptList = castToPoint3dList(geoParams.SelectToken("points"));
objectId = doc.Objects.AddPolyline(ptList);
break;
case "CIRCLE":
Point3d circleCenter = castToPoint3d(geoParams.SelectToken("center"));
double circleRadius = castToDouble(geoParams.SelectToken("radius"));
var circle = new Circle(circleCenter, circleRadius);
objectId = doc.Objects.AddCircle(circle);
break;
case "ARC":
Point3d arcCenter = castToPoint3d(geoParams.SelectToken("center"));
double arcRadius = castToDouble(geoParams.SelectToken("radius"));
double arcAngle = castToDouble(geoParams.SelectToken("angle"));
var arc = new Arc(new Plane(arcCenter, Vector3d.ZAxis), arcRadius, arcAngle * Math.PI / 180);
objectId = doc.Objects.AddArc(arc);
break;
case "ELLIPSE":
Point3d ellipseCenter = castToPoint3d(geoParams.SelectToken("center"));
double ellipseRadiusX = castToDouble(geoParams.SelectToken("radius_x"));
double ellipseRadiusY = castToDouble(geoParams.SelectToken("radius_y"));
var ellipse = new Ellipse(new Plane(ellipseCenter, Vector3d.ZAxis), ellipseRadiusX, ellipseRadiusY);
objectId = doc.Objects.AddEllipse(ellipse);
break;
case "CURVE":
List<Point3d> controlPoints = castToPoint3dList(geoParams.SelectToken("points"));
int degree = castToInt(geoParams.SelectToken("degree"));
var curve = Curve.CreateControlPointCurve(controlPoints, degree) ?? throw new InvalidOperationException("unable to create control point curve from given points");
objectId = doc.Objects.AddCurve(curve);
break;
case "BOX":
// parse size
double width = castToDouble(geoParams.SelectToken("width"));
double length = castToDouble(geoParams.SelectToken("length"));
double height = castToDouble(geoParams.SelectToken("height"));
double xSize = width, ySize = length, zSize = height;
Box box = new Box(
Plane.WorldXY,
new Interval(-xSize / 2, xSize / 2),
new Interval(-ySize / 2, ySize / 2),
new Interval(-zSize / 2, zSize / 2)
);
objectId = doc.Objects.AddBox(box);
break;
case "SPHERE":
// parse radius
double radius = castToDouble(geoParams.SelectToken("radius"));
// Create sphere at origin with specified radius
Sphere sphere = new Sphere(Point3d.Origin, radius);
// Convert sphere to BREP for adding to document
objectId = doc.Objects.AddBrep(sphere.ToBrep());
break;
case "CONE":
double coneRadius = castToDouble(geoParams.SelectToken("radius"));
double coneHeight = castToDouble(geoParams.SelectToken("height"));
bool coneCap = castToBool(geoParams.SelectToken("cap"));
Cone cone = new Cone(Plane.WorldXY, coneHeight, coneRadius);
Brep brep = Brep.CreateFromCone(cone, coneCap);
objectId = doc.Objects.AddBrep(brep);
break;
case "CYLINDER":
double cylinderRadius = castToDouble(geoParams.SelectToken("radius"));
double cylinderHeight = castToDouble(geoParams.SelectToken("height"));
bool cylinderCap = castToBool(geoParams.SelectToken("cap"));
Circle cylinderCircle = new Circle(Plane.WorldXY, cylinderRadius);
Cylinder cylinder = new Cylinder(cylinderCircle, cylinderHeight);
objectId = doc.Objects.AddBrep(cylinder.ToBrep(cylinderCap, cylinderCap));
break;
case "SURFACE":
int[] surfaceCount = castToIntArray(geoParams.SelectToken("count"));
List<Point3d> surfacePoints = castToPoint3dList(geoParams.SelectToken("points"));
int[] surfaceDegree = castToIntArray(geoParams.SelectToken("degree"));
bool[] surfaceClosed = castToBoolArray(geoParams.SelectToken("closed"));
var surf = NurbsSurface.CreateThroughPoints(surfacePoints, surfaceCount[0], surfaceCount[1], surfaceDegree[0], surfaceDegree[1], surfaceClosed[0], surfaceClosed[1]);
objectId = doc.Objects.AddSurface(surf);
break;
default:
throw new InvalidOperationException("Invalid object type");
}
if (objectId == Guid.Empty)
throw new InvalidOperationException("Failed to create object");
var rhinoObject = doc.Objects.Find(objectId);
if (rhinoObject != null)
{
if (!string.IsNullOrEmpty(name)) rhinoObject.Attributes.Name = name;
if (customColor)
{
rhinoObject.Attributes.ColorSource = ObjectColorSource.ColorFromObject;
rhinoObject.Attributes.ObjectColor = Color.FromArgb(color[0], color[1], color[2]);
}
doc.Objects.ModifyAttributes(rhinoObject, rhinoObject.Attributes, true);
}
// Update views
doc.Views.Redraw();
// apply modification
parameters["id"] = objectId;
return ModifyObject(parameters);
}
}
```
--------------------------------------------------------------------------------
/assets/rhinomcp_logo.svg:
--------------------------------------------------------------------------------
```
<svg width="359" height="362" viewBox="0 0 359 362" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="359" height="362" fill="#F6F6F6"/>
<path d="M216.5 89.5L140 62.5L116.5 85.5L187 110.187V163.5L116.5 139.155L93 162L187 194.5L251.5 132.5H216.5V89.5Z" fill="#303030" stroke="black"/>
<path d="M30.618 236.361H39.663V242.793H39.797C40.2437 241.721 40.8467 240.738 41.606 239.845C42.3653 238.907 43.2363 238.125 44.219 237.5C45.2017 236.83 46.2513 236.316 47.368 235.959C48.4847 235.602 49.646 235.423 50.852 235.423C51.4773 235.423 52.1697 235.535 52.929 235.758V244.602C52.4823 244.513 51.9463 244.446 51.321 244.401C50.6957 244.312 50.0927 244.267 49.512 244.267C47.77 244.267 46.296 244.557 45.09 245.138C43.884 245.719 42.9013 246.523 42.142 247.55C41.4273 248.533 40.9137 249.694 40.601 251.034C40.2883 252.374 40.132 253.826 40.132 255.389V271H30.618V236.361ZM56.659 223.162H66.173V241.185H66.374C67.58 239.175 69.121 237.723 70.997 236.83C72.873 235.892 74.7043 235.423 76.491 235.423C79.037 235.423 81.114 235.78 82.722 236.495C84.3747 237.165 85.67 238.125 86.608 239.376C87.546 240.582 88.1937 242.078 88.551 243.865C88.953 245.607 89.154 247.55 89.154 249.694V271H79.64V251.436C79.64 248.577 79.1933 246.456 78.3 245.071C77.4067 243.642 75.821 242.927 73.543 242.927C70.9523 242.927 69.0763 243.709 67.915 245.272C66.7537 246.791 66.173 249.314 66.173 252.843V271H56.659V223.162ZM106.157 231.001H96.6428V223.162H106.157V231.001ZM96.6428 236.361H106.157V271H96.6428V236.361ZM113.648 236.361H122.693V241.185H122.894C124.1 239.175 125.664 237.723 127.584 236.83C129.505 235.892 131.47 235.423 133.48 235.423C136.026 235.423 138.103 235.78 139.711 236.495C141.364 237.165 142.659 238.125 143.597 239.376C144.535 240.582 145.183 242.078 145.54 243.865C145.942 245.607 146.143 247.55 146.143 249.694V271H136.629V251.436C136.629 248.577 136.183 246.456 135.289 245.071C134.396 243.642 132.81 242.927 130.532 242.927C127.942 242.927 126.066 243.709 124.904 245.272C123.743 246.791 123.162 249.314 123.162 252.843V271H113.648V236.361ZM161.806 253.714C161.806 255.099 161.94 256.461 162.208 257.801C162.476 259.096 162.923 260.28 163.548 261.352C164.218 262.379 165.089 263.206 166.161 263.831C167.233 264.456 168.573 264.769 170.181 264.769C171.789 264.769 173.129 264.456 174.201 263.831C175.318 263.206 176.189 262.379 176.814 261.352C177.484 260.28 177.953 259.096 178.221 257.801C178.489 256.461 178.623 255.099 178.623 253.714C178.623 252.329 178.489 250.967 178.221 249.627C177.953 248.287 177.484 247.103 176.814 246.076C176.189 245.049 175.318 244.222 174.201 243.597C173.129 242.927 171.789 242.592 170.181 242.592C168.573 242.592 167.233 242.927 166.161 243.597C165.089 244.222 164.218 245.049 163.548 246.076C162.923 247.103 162.476 248.287 162.208 249.627C161.94 250.967 161.806 252.329 161.806 253.714ZM152.292 253.714C152.292 250.945 152.716 248.443 153.565 246.21C154.414 243.932 155.62 242.011 157.183 240.448C158.746 238.84 160.622 237.612 162.811 236.763C165 235.87 167.456 235.423 170.181 235.423C172.906 235.423 175.362 235.87 177.551 236.763C179.784 237.612 181.683 238.84 183.246 240.448C184.809 242.011 186.015 243.932 186.864 246.21C187.713 248.443 188.137 250.945 188.137 253.714C188.137 256.483 187.713 258.985 186.864 261.218C186.015 263.451 184.809 265.372 183.246 266.98C181.683 268.543 179.784 269.749 177.551 270.598C175.362 271.447 172.906 271.871 170.181 271.871C167.456 271.871 165 271.447 162.811 270.598C160.622 269.749 158.746 268.543 157.183 266.98C155.62 265.372 154.414 263.451 153.565 261.218C152.716 258.985 152.292 256.483 152.292 253.714ZM194.591 236.361H203.569V241.051H203.703C204.954 239.264 206.45 237.88 208.192 236.897C209.979 235.914 212.011 235.423 214.289 235.423C216.478 235.423 218.465 235.847 220.252 236.696C222.083 237.545 223.468 239.041 224.406 241.185C225.433 239.666 226.818 238.326 228.56 237.165C230.347 236.004 232.446 235.423 234.858 235.423C236.689 235.423 238.387 235.646 239.95 236.093C241.513 236.54 242.853 237.254 243.97 238.237C245.087 239.22 245.958 240.515 246.583 242.123C247.208 243.686 247.521 245.585 247.521 247.818V271H238.007V251.369C238.007 250.208 237.962 249.113 237.873 248.086C237.784 247.059 237.538 246.165 237.136 245.406C236.734 244.647 236.131 244.044 235.327 243.597C234.568 243.15 233.518 242.927 232.178 242.927C230.838 242.927 229.744 243.195 228.895 243.731C228.091 244.222 227.443 244.892 226.952 245.741C226.505 246.545 226.193 247.483 226.014 248.555C225.88 249.582 225.813 250.632 225.813 251.704V271H216.299V251.57C216.299 250.543 216.277 249.538 216.232 248.555C216.187 247.528 215.986 246.59 215.629 245.741C215.316 244.892 214.758 244.222 213.954 243.731C213.195 243.195 212.056 242.927 210.537 242.927C210.09 242.927 209.487 243.039 208.728 243.262C208.013 243.441 207.299 243.82 206.584 244.401C205.914 244.937 205.333 245.741 204.842 246.813C204.351 247.84 204.105 249.203 204.105 250.9V271H194.591V236.361ZM278.626 248.555C278 244.58 275.655 242.592 271.591 242.592C270.072 242.592 268.799 242.949 267.772 243.664C266.744 244.334 265.896 245.227 265.226 246.344C264.6 247.416 264.154 248.622 263.886 249.962C263.618 251.257 263.484 252.553 263.484 253.848C263.484 255.099 263.618 256.372 263.886 257.667C264.154 258.962 264.578 260.146 265.159 261.218C265.784 262.245 266.61 263.094 267.638 263.764C268.665 264.434 269.916 264.769 271.39 264.769C273.668 264.769 275.41 264.144 276.616 262.893C277.866 261.598 278.648 259.878 278.961 257.734H288.14C287.514 262.335 285.728 265.841 282.78 268.253C279.832 270.665 276.057 271.871 271.457 271.871C268.866 271.871 266.476 271.447 264.288 270.598C262.144 269.705 260.312 268.476 258.794 266.913C257.275 265.35 256.091 263.496 255.243 261.352C254.394 259.163 253.97 256.774 253.97 254.183C253.97 251.503 254.349 249.024 255.109 246.746C255.913 244.423 257.074 242.436 258.593 240.783C260.111 239.086 261.965 237.768 264.154 236.83C266.342 235.892 268.844 235.423 271.658 235.423C273.712 235.423 275.678 235.691 277.554 236.227C279.474 236.763 281.172 237.589 282.646 238.706C284.164 239.778 285.393 241.14 286.331 242.793C287.269 244.401 287.805 246.322 287.939 248.555H278.626ZM311.001 264.769C312.565 264.769 313.86 264.456 314.887 263.831C315.959 263.206 316.808 262.402 317.433 261.419C318.103 260.392 318.572 259.208 318.84 257.868C319.108 256.528 319.242 255.166 319.242 253.781C319.242 252.396 319.086 251.034 318.773 249.694C318.505 248.354 318.036 247.17 317.366 246.143C316.696 245.071 315.825 244.222 314.753 243.597C313.726 242.927 312.453 242.592 310.934 242.592C309.371 242.592 308.053 242.927 306.981 243.597C305.954 244.222 305.105 245.049 304.435 246.076C303.81 247.103 303.363 248.287 303.095 249.627C302.827 250.967 302.693 252.352 302.693 253.781C302.693 255.166 302.827 256.528 303.095 257.868C303.408 259.208 303.877 260.392 304.502 261.419C305.172 262.402 306.043 263.206 307.115 263.831C308.187 264.456 309.483 264.769 311.001 264.769ZM293.514 236.361H302.559V240.783H302.693C303.855 238.907 305.329 237.545 307.115 236.696C308.902 235.847 310.867 235.423 313.011 235.423C315.736 235.423 318.081 235.937 320.046 236.964C322.012 237.991 323.642 239.354 324.937 241.051C326.233 242.748 327.193 244.736 327.818 247.014C328.444 249.247 328.756 251.592 328.756 254.049C328.756 256.372 328.444 258.605 327.818 260.749C327.193 262.893 326.233 264.791 324.937 266.444C323.687 268.097 322.101 269.414 320.18 270.397C318.304 271.38 316.093 271.871 313.547 271.871C311.403 271.871 309.416 271.447 307.584 270.598C305.798 269.705 304.324 268.409 303.162 266.712H303.028V283.127H293.514V236.361Z" fill="#303030"/>
</svg>
```
--------------------------------------------------------------------------------
/rhino_mcp_server/src/rhinomcp/server.py:
--------------------------------------------------------------------------------
```python
# rhino_mcp_server.py
from mcp.server.fastmcp import FastMCP, Context, Image
import socket
import json
import asyncio
import logging
from dataclasses import dataclass
from contextlib import asynccontextmanager
from typing import AsyncIterator, Dict, Any, List
# Configure logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("RhinoMCPServer")
@dataclass
class RhinoConnection:
host: str
port: int
sock: socket.socket | None = None # Changed from 'socket' to 'sock' to avoid naming conflict
def connect(self) -> bool:
"""Connect to the Rhino addon socket server"""
if self.sock:
return True
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
logger.info(f"Connected to Rhino at {self.host}:{self.port}")
return True
except Exception as e:
logger.error(f"Failed to connect to Rhino: {str(e)}")
self.sock = None
return False
def disconnect(self):
"""Disconnect from the Rhino addon"""
if self.sock:
try:
self.sock.close()
except Exception as e:
logger.error(f"Error disconnecting from Rhino: {str(e)}")
finally:
self.sock = None
def receive_full_response(self, sock, buffer_size=8192):
"""Receive the complete response, potentially in multiple chunks"""
chunks = []
# Use a consistent timeout value that matches the addon's timeout
sock.settimeout(15.0) # Match the addon's timeout
try:
while True:
try:
chunk = sock.recv(buffer_size)
if not chunk:
# If we get an empty chunk, the connection might be closed
if not chunks: # If we haven't received anything yet, this is an error
raise Exception("Connection closed before receiving any data")
break
chunks.append(chunk)
# Check if we've received a complete JSON object
try:
data = b''.join(chunks)
json.loads(data.decode('utf-8'))
# If we get here, it parsed successfully
logger.info(f"Received complete response ({len(data)} bytes)")
return data
except json.JSONDecodeError:
# Incomplete JSON, continue receiving
continue
except socket.timeout:
# If we hit a timeout during receiving, break the loop and try to use what we have
logger.warning("Socket timeout during chunked receive")
break
except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
logger.error(f"Socket connection error during receive: {str(e)}")
raise # Re-raise to be handled by the caller
except socket.timeout:
logger.warning("Socket timeout during chunked receive")
except Exception as e:
logger.error(f"Error during receive: {str(e)}")
raise
# If we get here, we either timed out or broke out of the loop
# Try to use what we have
if chunks:
data = b''.join(chunks)
logger.info(f"Returning data after receive completion ({len(data)} bytes)")
try:
# Try to parse what we have
json.loads(data.decode('utf-8'))
return data
except json.JSONDecodeError:
# If we can't parse it, it's incomplete
raise Exception("Incomplete JSON response received")
else:
raise Exception("No data received")
def send_command(self, command_type: str, params: Dict[str, Any] = {}) -> Dict[str, Any]:
"""Send a command to Rhino and return the response"""
if not self.sock and not self.connect():
raise ConnectionError("Not connected to Rhino")
command = {
"type": command_type,
"params": params or {}
}
try:
# Log the command being sent
logger.info(f"Sending command: {command_type} with params: {params}")
if self.sock is None:
raise Exception("Socket is not connected")
# Send the command
self.sock.sendall(json.dumps(command).encode('utf-8'))
logger.info(f"Command sent, waiting for response...")
# Set a timeout for receiving - use the same timeout as in receive_full_response
self.sock.settimeout(15.0) # Match the addon's timeout
# Receive the response using the improved receive_full_response method
response_data = self.receive_full_response(self.sock)
logger.info(f"Received {len(response_data)} bytes of data")
response = json.loads(response_data.decode('utf-8'))
logger.info(f"Response parsed, status: {response.get('status', 'unknown')}")
if response.get("status") == "error":
logger.error(f"Rhino error: {response.get('message')}")
raise Exception(response.get("message", "Unknown error from Rhino"))
return response.get("result", {})
except socket.timeout:
logger.error("Socket timeout while waiting for response from Rhino")
# Don't try to reconnect here - let the get_rhino_connection handle reconnection
# Just invalidate the current socket so it will be recreated next time
self.sock = None
raise Exception("Timeout waiting for Rhino response - try simplifying your request")
except (ConnectionError, BrokenPipeError, ConnectionResetError) as e:
logger.error(f"Socket connection error: {str(e)}")
self.sock = None
raise Exception(f"Connection to Rhino lost: {str(e)}")
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON response from Rhino: {str(e)}")
# Try to log what was received
if 'response_data' in locals() and response_data: # type: ignore
logger.error(f"Raw response (first 200 bytes): {response_data[:200]}")
raise Exception(f"Invalid response from Rhino: {str(e)}")
except Exception as e:
logger.error(f"Error communicating with Rhino: {str(e)}")
# Don't try to reconnect here - let the get_rhino_connection handle reconnection
self.sock = None
raise Exception(f"Communication error with Rhino: {str(e)}")
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Manage server startup and shutdown lifecycle"""
# We don't need to create a connection here since we're using the global connection
# for resources and tools
try:
# Just log that we're starting up
logger.info("RhinoMCP server starting up")
# Try to connect to Rhino on startup to verify it's available
try:
# This will initialize the global connection if needed
rhino = get_rhino_connection()
logger.info("Successfully connected to Rhino on startup")
except Exception as e:
logger.warning(f"Could not connect to Rhino on startup: {str(e)}")
logger.warning("Make sure the Rhino addon is running before using Rhino resources or tools")
# Return an empty context - we're using the global connection
yield {}
finally:
# Clean up the global connection on shutdown
global _rhino_connection
if _rhino_connection:
logger.info("Disconnecting from Rhino on shutdown")
_rhino_connection.disconnect()
_rhino_connection = None
logger.info("RhinoMCP server shut down")
# Create the MCP server with lifespan support
mcp = FastMCP(
"RhinoMCP",
lifespan=server_lifespan
)
# Resource endpoints
# Global connection for resources (since resources can't access context)
_rhino_connection = None
def get_rhino_connection():
"""Get or create a persistent Rhino connection"""
global _rhino_connection
# Create a new connection if needed
if _rhino_connection is None:
_rhino_connection = RhinoConnection(host="127.0.0.1", port=1999)
if not _rhino_connection.connect():
logger.error("Failed to connect to Rhino")
_rhino_connection = None
raise Exception("Could not connect to Rhino. Make sure the Rhino addon is running.")
logger.info("Created new persistent connection to Rhino")
return _rhino_connection
# Main execution
def main():
"""Run the MCP server"""
mcp.run()
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/userdata.py:
--------------------------------------------------------------------------------
```python
import scriptcontext
from rhinoscript import utility as rhutil
def DeleteDocumentData(section=None, entry=None):
"""Removes user data strings from the current document
Parameters:
section (str, optional): section name. If omitted, all sections and their corresponding
entries are removed
entry (str, optional): entry name. If omitted, all entries for section are removed
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
rs.DeleteDocumentData( "MySection1", "MyEntry1" )
rs.DeleteDocumentData( "MySection1", "MyEntry2" )
rs.DeleteDocumentData( "MySection2", "MyEntry1" )
See Also:
DocumentDataCount
GetDocumentData
IsDocumentData
SetDocumentData
"""
return scriptcontext.doc.Strings.Delete(section, entry)
def DocumentDataCount():
"""Returns the number of user data strings in the current document
Returns:
number: the number of user data strings in the current document
Example:
import rhinoscriptsyntax as rs
count = rs.DocumentDataCount()
print("RhinoScript document user data count: {}".format(count))
See Also:
DeleteDocumentData
GetDocumentData
IsDocumentData
SetDocumentData
"""
return scriptcontext.doc.Strings.DocumentDataCount
def DocumentUserTextCount():
"""Returns the number of user text strings in the current document
Returns:
number: the number of user text strings in the current document
Example:
See Also:
GetDocumentUserText
IsDocumentUserText
SetDocumentUserText
"""
return scriptcontext.doc.Strings.DocumentUserTextCount
def GetDocumentData(section=None, entry=None):
"""Returns a user data item from the current document
Parameters:
section (str, optional): section name. If omitted, all section names are returned
entry (str, optional): entry name. If omitted, all entry names for section are returned
Returns:
list(str, ...): of all section names if section name is omitted
list(str, ...) of all entry names for a section if entry is omitted
str: value of the entry if both section and entry are specified
None: if not successful
Example:
import rhinoscriptsyntax as rs
value = rs.GetDocumentData("MySection1", "MyEntry1")
print(value)
value = rs.GetDocumentData("MySection1", "MyEntry2")
print(value)
value = rs.GetDocumentData("MySection2", "MyEntry1")
print(value)
See Also:
DeleteDocumentData
DocumentDataCount
IsDocumentData
SetDocumentData
"""
if section is None:
rc = scriptcontext.doc.Strings.GetSectionNames()
return list(rc) if rc else None
if entry is None:
rc = scriptcontext.doc.Strings.GetEntryNames(section)
return list(rc) if rc else None
val = scriptcontext.doc.Strings.GetValue(section, entry)
return val if val else None
def GetDocumentUserText(key=None):
"""Returns user text stored in the document
Parameters:
key (str, optional): key to use for retrieving user text. If empty, all keys are returned
Returns:
str: If key is specified, then the associated value if successful.
list(str, ...):If key is not specified, then a list of key names if successful.
None: If not successful, or on error.
Example:
import rhinoscriptsyntax as rs
print(rs.GetDocumentUserText("Designer"))
print(rs.GetDocumentUserText("Notes"))
See Also:
SetDocumentUserText
"""
if key:
val = scriptcontext.doc.Strings.GetValue(key)
return val if val else None
#todo: leaky abstraction: "\\" logic should be inside doc.Strings implementation
keys = [scriptcontext.doc.Strings.GetKey(i) for i in range(scriptcontext.doc.Strings.Count) if not "\\" in scriptcontext.doc.Strings.GetKey(i)]
return keys if keys else None
def GetUserText(object_id, key=None, attached_to_geometry=False):
"""Returns user text stored on an object.
Parameters:
object_id (guid): the object's identifies
key (str, optional): the key name. If omitted all key names for an object are returned
attached_to_geometry (bool, optional): location on the object to retrieve the user text
Returns:
str: if key is specified, the associated value if successful
list(str, ...): if key is not specified, a list of key names if successful
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if obj:
print(rs.GetUserText(obj, "PartNo"))
print(rs.GetUserText(obj, "Price"))
See Also:
IsUserText
SetUserText
"""
obj = rhutil.coercerhinoobject(object_id, True, True)
source = None
if attached_to_geometry: source = obj.Geometry
else: source = obj.Attributes
rc = None
if key: return source.GetUserString(key)
userstrings = source.GetUserStrings()
return [userstrings.GetKey(i) for i in range(userstrings.Count)]
def IsDocumentData():
"""Verifies the current document contains user data
Returns:
bool: True or False indicating the presence of Script user data
Example:
import rhinoscriptsyntax as rs
result = rs.IsDocumentData()
if result:
print("This document contains Script document user data")
else:
print("This document contains no Script document user data")
See Also:
DeleteDocumentData
DocumentDataCount
GetDocumentData
SetDocumentData
"""
return scriptcontext.doc.Strings.DocumentDataCount > 0
def IsDocumentUserText():
"""Verifies the current document contains user text
Returns:
bool: True or False indicating the presence of Script user text
Example:
See Also:
GetDocumentUserText
SetDocumentUserText
"""
return scriptcontext.doc.Strings.DocumentUserTextCount > 0
def IsUserText(object_id):
"""Verifies that an object contains user text
Parameters:
object_id (guid): the object's identifier
Returns:
number: result of test:
0 = no user text
1 = attribute user text
2 = geometry user text
3 = both attribute and geometry user text
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if obj:
usertext_type = rs.IsUserText(obj)
if usertext_type==0: print("Object has no user text")
elif usertext_type==1: print("Object has attribute user text")
elif usertext_type==2: print("Object has geometry user text")
elif usertext_type==3: print("Object has attribute and geometry user text")
else: print("Object does not exist")
See Also:
GetUserText
SetUserText
"""
obj = rhutil.coercerhinoobject(object_id, True, True)
rc = 0
if obj.Attributes.UserStringCount: rc = rc|1
if obj.Geometry.UserStringCount: rc = rc|2
return rc
def SetDocumentData(section, entry, value):
"""Adds or sets a user data string to the current document
Parameters:
section (str): the section name
entry (str): the entry name
value (str): the string value
Returns:
str: The previous value
Example:
import rhinoscriptsyntax as rs
rs.SetDocumentData( "MySection1", "MyEntry1", "MyValue1" )
rs.SetDocumentData( "MySection1", "MyEntry2", "MyValue2" )
rs.SetDocumentData( "MySection2", "MyEntry1", "MyValue1" )
See Also:
DeleteDocumentData
DocumentDataCount
GetDocumentData
IsDocumentData
"""
val = scriptcontext.doc.Strings.SetString(section, entry, value)
return val if val else None
def SetDocumentUserText(key, value=None):
"""Sets or removes user text stored in the document
Parameters:
key (str): key name to set
value (str): The string value to set. If omitted the key/value pair
specified by key will be deleted
Returns:
bool: True or False indicating success
Example:
import rhinoscriptsyntax as rs
rs.SetDocumentUserText("Designer", "Steve Baer")
rs.SetDocumentUserText("Notes", "Added some layer and updated some geometry")
See Also:
GetDocumentUserText
"""
if value: scriptcontext.doc.Strings.SetString(key,value)
else: scriptcontext.doc.Strings.Delete(key)
return True
def SetUserText(object_id, key, value=None, attach_to_geometry=False):
"""Sets or removes user text stored on an object.
Parameters:
object_id (str): the object's identifier
key (str): the key name to set
value (str, optional) the string value to set. If omitted, the key/value pair
specified by key will be deleted
attach_to_geometry (bool, optional): location on the object to store the user text
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if obj:
rs.SetUserText( obj, "PartNo", "KM40-4960" )
rs.SetUserText( obj, "Price", "1.25" )
See Also:
GetUserText
IsUserText
"""
obj = rhutil.coercerhinoobject(object_id, True, True)
if type(key) is not str: key = str(key)
if value and type(value) is not str: value = str(value)
if attach_to_geometry: return obj.Geometry.SetUserString(key, value)
return obj.Attributes.SetUserString(key, value)
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/line.py:
--------------------------------------------------------------------------------
```python
import Rhino
import scriptcontext
import rhinocompat as compat
from rhinoscript import utility as rhutil
def LineClosestPoint(line, testpoint):
"""Finds the point on an infinite line that is closest to a test point
Parameters:
line ([point, point]): List of 6 numbers or 2 Point3d. Two 3-D points identifying the starting and ending points of the line.
testpoint (point): List of 3 numbers or Point3d. The test point.
Returns:
point: the point on the line that is closest to the test point if successful, otherwise None
Example:
import rhinoscriptsyntax as rs
line = (0,0,0), (5,5,0)
point = (15, 10, 0)
result = rs.LineClosestPoint( line, point)
if result: rs.AddPoint(result)
See Also:
LineIsFartherThan
LineMaxDistanceTo
LineMinDistanceTo
LinePlane
LineTransform
"""
line = rhutil.coerceline(line, True)
testpoint = rhutil.coerce3dpoint(testpoint, True)
return line.ClosestPoint(testpoint, False)
def LineCylinderIntersection(line, cylinder_plane, cylinder_height, cylinder_radius):
"""Calculates the intersection of a line and a cylinder
Parameters:
line (guid|line): the line to intersect
cylinder_plane (plane): base plane of the cylinder
cylinder_height (number): height of the cylinder
cylinder_radius (number): radius of the cylinder
Returns:
list(point, ...): list of intersection points (0, 1, or 2 points)
Example:
import rhinoscriptsyntax as rs
plane = rs.WorldXYPlane()
line = (-10,0,0), (10,0,10)
points = rs.LineCylinderIntersection(line, plane, cylinder_height=10, cylinder_radius=5)
if points:
for point in points: rs.AddPoint(point)
See Also:
LineLineIntersection
LinePlaneIntersection
LineSphereIntersection
"""
line = rhutil.coerceline(line, True)
cylinder_plane = rhutil.coerceplane(cylinder_plane, True)
circle = Rhino.Geometry.Circle( cylinder_plane, cylinder_radius )
if not circle.IsValid: raise ValueError("unable to create valid circle with given plane and radius")
cyl = Rhino.Geometry.Cylinder( circle, cylinder_height )
if not cyl.IsValid: raise ValueError("unable to create valid cylinder with given circle and height")
rc, pt1, pt2 = Rhino.Geometry.Intersect.Intersection.LineCylinder(line, cyl)
if rc==compat.ENUM_NONE(Rhino.Geometry.Intersect.LineCylinderIntersection):
return []
if rc==Rhino.Geometry.Intersect.LineCylinderIntersection.Single:
return [pt1]
return [pt1, pt2]
def LineIsFartherThan(line, distance, point_or_line):
"""Determines if the shortest distance from a line to a point or another
line is greater than a specified distance
Parameters:
line (line | [point, point]): List of 6 numbers, 2 Point3d, or Line.
distance (number): the distance
point_or_line (point|line) the test point or the test line
Returns:
bool: True if the shortest distance from the line to the other project is
greater than distance, False otherwise
None: on error
Example:
import rhinoscriptsyntax as rs
line = (0,0,0), (10,10,0)
testPoint = (10,5,0)
print(rs.LineIsFartherThan(line, 3, testPoint))
See Also:
LineClosestPoint
LineMaxDistanceTo
LineMinDistanceTo
LinePlane
LineTransform
"""
line = rhutil.coerceline(line, True)
test = rhutil.coerceline(point_or_line)
if not test: test = rhutil.coerce3dpoint(point_or_line, True)
minDist = line.MinimumDistanceTo(test)
return minDist>distance
def LineLineIntersection(lineA, lineB):
"""Calculates the intersection of two non-parallel lines. Note, the two
lines do not have to intersect for an intersection to be found. (see help)
Parameters:
lineA, lineB (line): lines to intersect
Returns:
tuple(point, point): containing a point on the first line and a point on the second line if successful
None: on error
Example:
import rhinoscriptsyntax as rs
lineA = (1,1,0), (5,0,0)
lineB = (1,3,0), (5,5,0)
point = rs.LineLineIntersection(lineA, lineB)
if point:
rs.AddPoint(point[0])
rs.AddPoint(point[1])
See Also:
IntersectPlanes
LinePlaneIntersection
PlanePlaneIntersection
"""
lineA = rhutil.coerceline(lineA, True)
lineB = rhutil.coerceline(lineB, True)
rc, a, b = Rhino.Geometry.Intersect.Intersection.LineLine(lineA, lineB)
if not rc: return None
return lineA.PointAt(a), lineB.PointAt(b)
def LineMaxDistanceTo(line, point_or_line):
"""Finds the longest distance between a line as a finite chord, and a point
or another line
Parameters:
line (line | [point, point]): List of 6 numbers, two Point3d, or Line.
point_or_line (point|line): the test point or test line.
Returns:
number: A distance (D) such that if Q is any point on the line and P is any point on the other object, then D >= Rhino.Distance(Q, P).
None: on error
Example:
import rhinoscriptsyntax as rs
line = (0,0,0), (10,10,0)
print(rs.LineMaxDistanceTo( line, (10,5,0) ))
See Also:
LineClosestPoint
LineIsFartherThan
LineMinDistanceTo
LinePlane
LineTransform
"""
line = rhutil.coerceline(line, True)
test = rhutil.coerceline(point_or_line)
if test is None: test = rhutil.coerce3dpoint(point_or_line, True)
return line.MaximumDistanceTo(test)
def LineMinDistanceTo(line, point_or_line):
"""Finds the shortest distance between a line as a finite chord, and a point
or another line
Parameters:
line (line | [point, point]): List of 6 numbers, two Point3d, or Line.
point_or_line (point|line): the test point or test line.
Returns:
number: A distance (D) such that if Q is any point on the line and P is any point on the other object, then D <= Rhino.Distance(Q, P).
None: on error
Example:
import rhinoscriptsyntax as rs
line = (0,0,0), (10,10,0)
print(rs.LineMinDistanceTo(line, (10,5,0)))
See Also:
LineClosestPoint
LineIsFartherThan
LineMaxDistanceTo
LinePlane
LineTransform
"""
line = rhutil.coerceline(line, True)
test = rhutil.coerceline(point_or_line)
if test is None: test = rhutil.coerce3dpoint(point_or_line, True)
return line.MinimumDistanceTo(test)
def LinePlane(line):
"""Returns a plane that contains the line. The origin of the plane is at the start of
the line. If possible, a plane parallel to the world XY, YZ, or ZX plane is returned
Parameters:
line (line | [point, point]): List of 6 numbers, two Point3d, or Line.
Returns:
plane: the plane if successful
None: if not successful
Example:
import rhinoscriptsyntax as rs
lineFrom = (0,0,0)
lineTo = (10,10,0)
distance = rs.Distance(lineFrom, lineTo)
plane = rs.LinePlane([lineFrom, lineTo])
rs.AddPlaneSurface( plane, distance, distance )
See Also:
LineClosestPoint
LineIsFartherThan
LineMaxDistanceTo
LineMinDistanceTo
LineTransform
"""
line = rhutil.coerceline(line, True)
rc, plane = line.TryGetPlane()
if not rc: return scriptcontext.errorhandler()
return plane
def LinePlaneIntersection(line, plane):
"""Calculates the intersection of a line and a plane.
Parameters:
line ([point, point]): Two 3D points identifying the starting and ending points of the line to intersect.
plane (plane): The plane to intersect.
Returns:
point: The 3D point of intersection is successful.
None: if not successful, or on error.
Example:
import rhinoscriptsyntax as rs
plane = rs.WorldXYPlane()
line = (2, 11, 13), (20, 4, -10)
point = rs.LinePlaneIntersection(line, plane)
if( point!=None ): rs.AddPoint(point)
See Also:
LineLineIntersection
PlanePlaneIntersection
"""
plane = rhutil.coerceplane(plane, True)
line_points = rhutil.coerce3dpointlist(line, True)
line = Rhino.Geometry.Line(line_points[0], line_points[1])
rc, t = Rhino.Geometry.Intersect.Intersection.LinePlane(line, plane)
if not rc: return scriptcontext.errorhandler()
return line.PointAt(t)
def LineSphereIntersection(line, sphere_center, sphere_radius):
"""Calculates the intersection of a line and a sphere
Parameters:
line (line | [point, point]): the line
sphere_center (point): the center point of the sphere
sphere_radius (number): the radius of the sphere
Returns:
list(point, ...): list of intersection points if successful, otherwise None
Example:
import rhinoscriptsyntax as rs
radius = 10
line = (-10,0,0), (10,0,10)
points = rs.LineSphereIntersection(line, (0,0,0), radius)
if points:
for point in points: rs.AddPoint(point)
See Also:
LineCylinderIntersection
LineLineIntersection
LinePlaneIntersection
"""
line = rhutil.coerceline(line, True)
sphere_center = rhutil.coerce3dpoint(sphere_center, True)
sphere = Rhino.Geometry.Sphere(sphere_center, sphere_radius)
rc, pt1, pt2 = Rhino.Geometry.Intersect.Intersection.LineSphere(line, sphere)
if rc==compat.ENUM_NONE(Rhino.Geometry.Intersect.LineSphereIntersection): return []
if rc==Rhino.Geometry.Intersect.LineSphereIntersection.Single: return [pt1]
return [pt1, pt2]
def LineTransform(line, xform):
"""Transforms a line
Parameters:
line (guid): the line to transform
xform (transform): the transformation to apply
Returns:
guid: transformed line
Example:
import rhinoscriptsyntax as rs
line = (0,0,0), (10,10,0)
rs.AddLine( line[0], line[1] )
plane = rs.WorldXYPlane()
xform = rs.XformRotation(30, plane.Zaxis, plane.Origin)
line = rs.LineTransform(line, xform)
rs.AddLine( line.From, line.To )
See Also:
LineClosestPoint
LineIsFartherThan
LineMaxDistanceTo
LineMinDistanceTo
LinePlane
"""
line = rhutil.coerceline(line, True)
xform = rhutil.coercexform(xform, True)
success = line.Transform(xform)
if not success: raise Exception("unable to transform line")
return line
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/toolbar.py:
--------------------------------------------------------------------------------
```python
import Rhino
def CloseToolbarCollection(name, prompt=False):
"""Closes a currently open toolbar collection
Parameters:
name (str): name of a currently open toolbar collection
prompt (bool, optional): if True, user will be prompted to save the collection file
if it has been modified prior to closing
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
names = rs.ToolbarCollectionNames()
if names:
for name in names: rs.CloseToolbarCollection( name, True )
See Also:
IsToolbarCollection
OpenToolbarCollection
ToolbarCollectionCount
ToolbarCollectionNames
ToolbarCollectionPath
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile: return tbfile.Close(prompt)
return False
def HideToolbar(name, toolbar_group):
"""Hides a previously visible toolbar group in an open toolbar collection
Parameters:
name (str): name of a currently open toolbar file
toolbar_group (str): name of a toolbar group to hide
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
file = "C:\\SteveBaer\\AppData\\Roaming\\McNeel\\Rhinoceros\\5.0\\UI\\default.rui"
name = rs.IsToolbarCollection(file)
if names: rs.HideToolbar(name, "Layer")
See Also:
IsToolbar
IsToolbarVisible
ShowToolbar
ToolbarCount
ToolbarNames
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
group = tbfile.GetGroup(toolbar_group)
if group:
group.Visible = False
return True
return False
def IsToolbar(name, toolbar, group=False):
"""Verifies a toolbar (or toolbar group) exists in an open collection file
Parameters:
name (str): name of a currently open toolbar file
toolbar (str): name of a toolbar group
group (bool, optional): if toolbar parameter is referring to a toolbar group
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
file = "C:\\SteveBaer\\AppData\\Roaming\\McNeel\\Rhinoceros\\5.0\\UI\\default.rui"
name = rs.IsToolbarCollection(file)
if name:
if rs.IsToolbar(name, "Layer"):
print("The collection contains the Layer toolbar.")
else:
print("The collection does not contain the Layer toolbar.")
See Also:
HideToolbar
IsToolbarVisible
ShowToolbar
ToolbarCount
ToolbarNames
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
if group: return tbfile.GetGroup(toolbar) != None
return tbfile.GetToolbar(toolbar) != None
return False
def IsToolbarCollection(file):
"""Verifies that a toolbar collection is open
Parameters:
file (str): full path to a toolbar collection file
Returns:
str: Rhino-assigned name of the toolbar collection if successful
None: if not successful
Example:
import rhinoscriptsyntax as rs
file = "C:\\SteveBaer\\AppData\\Roaming\\McNeel\\Rhinoceros\\5.0\\UI\\default.rui"
name = rs.IsToolbarCollection(file)
if name: print("The default toolbar collection is loaded.")
else: print("The default toolbar collection is not loaded.")
See Also:
CloseToolbarCollection
OpenToolbarCollection
ToolbarCollectionCount
ToolbarCollectionNames
ToolbarCollectionPath
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByPath(file)
if tbfile: return tbfile.Name
def IsToolbarDocked(name, toolbar_group):
"""Verifies that a toolbar group in an open toolbar collection is visible
Parameters:
name (str): name of a currently open toolbar file
toolbar_group (str): name of a toolbar group
Returns:
boolean: True or False indicating success or failure
None: on error
Example:
import rhinoscriptsyntax as rs
rc = rs.IsToolbarDocked("Default", "Main1")
if rc==True:
print("The Main1 toolbar is docked.")
elif rc==False:
print("The Main1 toolbar is not docked.")
else:
print("The Main1 toolbar is not visible.")
See Also:
IsToolbar
IsToolbarVisible
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
group = tbfile.GetGroup(toolbar_group)
if group: return group.IsDocked
def IsToolbarVisible(name, toolbar_group):
"""Verifies that a toolbar group in an open toolbar collection is visible
Parameters:
name (str): name of a currently open toolbar file
toolbar_group (str): name of a toolbar group
Returns:
bool:True or False indicating success or failure
None: on error
Example:
import rhinoscriptsyntax as rs
file = "C:\\SteveBaer\\AppData\\Roaming\\McNeel\\Rhinoceros\\5.0\\UI\\default.rui"
name = rs.IsToolbarCollection(file)
if name:
if rs.IsToolbarVisible(name, "Layer"): print("The Layer toolbar is visible.")
else: print("The Layer toolbar is not visible.")
See Also:
HideToolbar
IsToolbar
ShowToolbar
ToolbarCount
ToolbarNames
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
group = tbfile.GetGroup(toolbar_group)
if group: return group.Visible
def OpenToolbarCollection(file):
"""Opens a toolbar collection file
Parameters:
file (str): full path to the collection file
Returns:
str: Rhino-assigned name of the toolbar collection if successful
None: if not successful
Example:
import rhinoscriptsyntax as rs
file = "C:\\SteveBaer\\AppData\\Roaming\\McNeel\\Rhinoceros\\5.0\\UI\\default.rui"
name = rs.IsToolbarCollection(file)
if name is None: rs.OpenToolbarCollection(file)
See Also:
CloseToolbarCollection
IsToolbarCollection
ToolbarCollectionCount
ToolbarCollectionNames
ToolbarCollectionPath
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.Open(file)
if tbfile: return tbfile.Name
def SaveToolbarCollection(name):
"""Saves an open toolbar collection to disk
Parameters:
name (str): name of a currently open toolbar file
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
name = "Default"
rs.SaveToolbarCollection(name)
See Also:
SaveToolbarCollectionAs
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile: return tbfile.Save()
return False
def SaveToolbarCollectionAs(name, file):
"""Saves an open toolbar collection to a different disk file
Parameters:
name (str): name of a currently open toolbar file
file (str): full path to file name to save to
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
name = "Default"
file = "D:\\NewDefault.rui"
rs.SaveToolbarCollectionAs(name,file)
See Also:
SaveToolbarCollection
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile: return tbfile.SaveAs(file)
return False
def ShowToolbar(name, toolbar_group):
"""Shows a previously hidden toolbar group in an open toolbar collection
Parameters:
name (str): name of a currently open toolbar file
toolbar_group (str): name of a toolbar group to show
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
file = "C:\\SteveBaer\\AppData\\Roaming\\McNeel\\Rhinoceros\\5.0\\UI\\default.rui"
name = rs.IsToolbarCollection(file)
if name: rs.ShowToolbar(name, "Layer")
See Also:
HideToolbar
IsToolbar
IsToolbarVisible
ToolbarCount
ToolbarNames
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
group = tbfile.GetGroup(toolbar_group)
if group:
group.Visible = True
return True
return False
def ToolbarCollectionCount():
"""Returns number of currently open toolbar collections
Returns:
number: the number of currently open toolbar collections
Example:
import rhinoscriptsyntax as rs
count = rs.ToolbarCollectionCount()
print("There are {} toolbar(s) collections loaded".format(count))
See Also:
CloseToolbarCollection
IsToolbarCollection
OpenToolbarCollection
ToolbarCollectionNames
ToolbarCollectionPath
"""
return Rhino.RhinoApp.ToolbarFiles.Count
def ToolbarCollectionNames():
"""Returns names of all currently open toolbar collections
Returns:
list(str, ...): the names of all currently open toolbar collections
Example:
import rhinoscriptsyntax as rs
names = rs.ToolbarCollectionNames()
if names:
for name in names: print(name)
See Also:
CloseToolbarCollection
IsToolbarCollection
OpenToolbarCollection
ToolbarCollectionCount
ToolbarCollectionPath
"""
return [tbfile.Name for tbfile in Rhino.RhinoApp.ToolbarFiles]
def ToolbarCollectionPath(name):
"""Returns full path to a currently open toolbar collection file
Parameters:
name (str): name of currently open toolbar collection
Returns:
str: full path on success
None: on error
Example:
import rhinoscriptsyntax as rs
names = rs.ToolbarCollectionNames()
if names:
for name in names: print(rs.ToolbarCollectionPath(name))
See Also:
CloseToolbarCollection
IsToolbarCollection
OpenToolbarCollection
ToolbarCollectionCount
ToolbarCollectionNames
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile: return tbfile.Path
def ToolbarCount(name, groups=False):
"""Returns the number of toolbars or groups in a currently open toolbar file
Parameters:
name (str): name of currently open toolbar collection
groups (bool, optional): If true, return the number of toolbar groups in the file
Returns:
number: number of toolbars on success
None: on error
Example:
import rhinoscriptsyntax as rs
names = rs.ToolbarCollectionNames()
if names:
count = rs.ToolbarCount(names[0])
print("The {} collection contains {} toolbars.".format(names[0], count))
See Also:
HideToolbar
IsToolbar
IsToolbarVisible
ShowToolbar
ToolbarNames
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
if groups: return tbfile.GroupCount
return tbfile.ToolbarCount
def ToolbarNames(name, groups=False):
"""Returns the names of all toolbars (or toolbar groups) found in a
currently open toolbar file
Parameters:
name (str): name of currently open toolbar collection
groups (bool, optional): If true, return the names of toolbar groups in the file
Returns:
list(str, ...): names of all toolbars (or toolbar groups) on success
None: on error
Example:
import rhinoscriptsytax as rs
names = rs.ToolbarCollectionNames()
if names:
toolbars = rs.ToolbarNames(names[0])
if toolbars:
for toolbar in toolbars: print(toolbar)
See Also:
HideToolbar
IsToolbar
IsToolbarVisible
ShowToolbar
ToolbarCount
"""
tbfile = Rhino.RhinoApp.ToolbarFiles.FindByName(name, True)
if tbfile:
rc = []
if groups:
for i in range(tbfile.GroupCount): rc.append(tbfile.GetGroup(i).Name)
else:
for i in range(tbfile.ToolbarCount): rc.append(tbfile.GetToolbar(i).Name)
return rc;
```
--------------------------------------------------------------------------------
/rhino_mcp_plugin/RhinoMCPServer.cs:
--------------------------------------------------------------------------------
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Rhino;
using Rhino.Commands;
using Rhino.Geometry;
using Rhino.Input;
using Rhino.Input.Custom;
using Rhino.UI;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.Text.Json;
using Rhino.DocObjects;
using rhinomcp.Serializers;
using JsonException = Newtonsoft.Json.JsonException;
using Eto.Forms;
using RhinoMCPPlugin.Functions;
namespace RhinoMCPPlugin
{
public class RhinoMCPServer
{
private string host;
private int port;
private bool running;
private TcpListener listener;
private Thread serverThread;
private readonly object lockObject = new object();
private RhinoMCPFunctions handler;
public RhinoMCPServer(string host = "127.0.0.1", int port = 1999)
{
this.host = host;
this.port = port;
this.running = false;
this.listener = null;
this.serverThread = null;
this.handler = new RhinoMCPFunctions();
}
public void Start()
{
lock (lockObject)
{
if (running)
{
RhinoApp.WriteLine("Server is already running");
return;
}
running = true;
}
try
{
// Create TCP listener
IPAddress ipAddress = IPAddress.Parse(host);
listener = new TcpListener(ipAddress, port);
listener.Start();
// Start server thread
serverThread = new Thread(ServerLoop);
serverThread.IsBackground = true;
serverThread.Start();
RhinoApp.WriteLine($"RhinoMCP server started on {host}:{port}");
}
catch (Exception e)
{
RhinoApp.WriteLine($"Failed to start server: {e.Message}");
Stop();
}
}
public void Stop()
{
lock (lockObject)
{
running = false;
}
// Close listener
if (listener != null)
{
try
{
listener.Stop();
}
catch
{
// Ignore errors on closing
}
listener = null;
}
// Wait for thread to finish
if (serverThread != null && serverThread.IsAlive)
{
try
{
serverThread.Join(1000); // Wait up to 1 second
}
catch
{
// Ignore errors on join
}
serverThread = null;
}
RhinoApp.WriteLine("RhinoMCP server stopped");
}
private void ServerLoop()
{
RhinoApp.WriteLine("Server thread started");
while (IsRunning())
{
try
{
// Set a timeout to check running condition periodically
listener.Server.ReceiveTimeout = 1000;
listener.Server.SendTimeout = 1000;
// Wait for client connection
if (listener.Pending())
{
TcpClient client = listener.AcceptTcpClient();
RhinoApp.WriteLine($"Connected to client: {client.Client.RemoteEndPoint}");
// Handle client in a separate thread
Thread clientThread = new Thread(() => HandleClient(client));
clientThread.IsBackground = true;
clientThread.Start();
}
else
{
// No pending connections, sleep a bit to prevent CPU overuse
Thread.Sleep(100);
}
}
catch (Exception e)
{
RhinoApp.WriteLine($"Error in server loop: {e.Message}");
if (!IsRunning())
break;
Thread.Sleep(500);
}
}
RhinoApp.WriteLine("Server thread stopped");
}
private bool IsRunning()
{
lock (lockObject)
{
return running;
}
}
private void HandleClient(TcpClient client)
{
RhinoApp.WriteLine("Client handler started");
byte[] buffer = new byte[8192];
string incompleteData = string.Empty;
try
{
NetworkStream stream = client.GetStream();
while (IsRunning())
{
try
{
// Check if there's data available to read
if (client.Available > 0 || stream.DataAvailable)
{
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
RhinoApp.WriteLine("Client disconnected");
break;
}
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
incompleteData += data;
try
{
// Try to parse as JSON
JObject command = JObject.Parse(incompleteData);
incompleteData = string.Empty;
// Execute command on Rhino's main thread
RhinoApp.InvokeOnUiThread(new Action(() =>
{
try
{
JObject response = ExecuteCommand(command);
string responseJson = JsonConvert.SerializeObject(response);
try
{
byte[] responseBytes = Encoding.UTF8.GetBytes(responseJson);
stream.Write(responseBytes, 0, responseBytes.Length);
}
catch
{
RhinoApp.WriteLine("Failed to send response - client disconnected");
}
}
catch (Exception e)
{
RhinoApp.WriteLine($"Error executing command: {e.Message}");
try
{
JObject errorResponse = new JObject
{
["status"] = "error",
["message"] = e.Message
};
byte[] errorBytes = Encoding.UTF8.GetBytes(errorResponse.ToString());
stream.Write(errorBytes, 0, errorBytes.Length);
}
catch
{
// Ignore send errors
}
}
}));
}
catch (JsonException)
{
// Incomplete JSON data, wait for more
}
}
else
{
// No data available, sleep a bit to prevent CPU overuse
Thread.Sleep(50);
}
}
catch (Exception e)
{
RhinoApp.WriteLine($"Error receiving data: {e.Message}");
break;
}
}
}
catch (Exception e)
{
RhinoApp.WriteLine($"Error in client handler: {e.Message}");
}
finally
{
try
{
client.Close();
}
catch
{
// Ignore errors on close
}
RhinoApp.WriteLine("Client handler stopped");
}
}
private JObject ExecuteCommand(JObject command)
{
try
{
string cmdType = command["type"]?.ToString();
JObject parameters = command["params"] as JObject ?? new JObject();
RhinoApp.WriteLine($"Executing command: {cmdType}");
JObject result = ExecuteCommandInternal(cmdType, parameters);
RhinoApp.WriteLine("Command execution complete");
return result;
}
catch (Exception e)
{
RhinoApp.WriteLine($"Error executing command: {e.Message}");
return new JObject
{
["status"] = "error",
["message"] = e.Message
};
}
}
private JObject ExecuteCommandInternal(string cmdType, JObject parameters)
{
// Dictionary to map command types to handler methods
Dictionary<string, Func<JObject, JObject>> handlers = new Dictionary<string, Func<JObject, JObject>>
{
["get_document_info"] = this.handler.GetDocumentInfo,
["create_object"] = this.handler.CreateObject,
["create_objects"] = this.handler.CreateObjects,
["get_object_info"] = this.handler.GetObjectInfo,
["get_selected_objects_info"] = this.handler.GetSelectedObjectsInfo,
["delete_object"] = this.handler.DeleteObject,
["modify_object"] = this.handler.ModifyObject,
["modify_objects"] = this.handler.ModifyObjects,
["execute_rhinoscript_python_code"] = this.handler.ExecuteRhinoscript,
["select_objects"] = this.handler.SelectObjects,
["create_layer"] = this.handler.CreateLayer,
["get_or_set_current_layer"] = this.handler.GetOrSetCurrentLayer,
["delete_layer"] = this.handler.DeleteLayer
// Add more handlers as needed
};
if (handlers.TryGetValue(cmdType, out var handler))
{
var doc = RhinoDoc.ActiveDoc;
var record = doc.BeginUndoRecord("Run MCP command");
try
{
JObject result = handler(parameters);
return new JObject
{
["status"] = "success",
["result"] = result
};
}
catch (Exception e)
{
RhinoApp.WriteLine($"Error in handler: {e.Message}");
return new JObject
{
["status"] = "error",
["message"] = e.Message
};
}
finally
{
doc.EndUndoRecord(record);
}
}
else
{
return new JObject
{
["status"] = "error",
["message"] = $"Unknown command type: {cmdType}"
};
}
}
}
}
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/group.py:
--------------------------------------------------------------------------------
```python
import scriptcontext
from rhinoscript import utility as rhutil
def AddGroup(group_name=None):
"""Adds a new empty group to the document
Parameters:
group_name (str, optional): name of the new group. If omitted, rhino automatically
generates the group name
Returns:
str: name of the new group if successful
None: is not successful or on error
Example:
import rhinoscriptsyntax as rs
name = rs.AddGroup("NewGroup")
See Also:
DeleteGroup
GroupCount
GroupNames
IsGroup
RenameGroup
"""
index = -1
if group_name is None:
index = scriptcontext.doc.Groups.Add()
else:
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Add( group_name )
rc = scriptcontext.doc.Groups.GroupName(index)
if rc is None: return scriptcontext.errorhandler()
return rc
def AddObjectsToGroup(object_ids, group_name):
"""Adds one or more objects to an existing group.
Parameters:
object_ids ([guid, ...]) list of Strings or Guids representing the object identifiers
group_name (str): the name of an existing group
Returns:
number: number of objects added to the group
Example:
import rhinoscriptsyntax as rs
name = "NewGroup"
object_ids = rs.GetObjects("Select objects to add to group")
if object_ids: rs.AddObjectsToGroup(object_ids, name)
See Also:
AddObjectToGroup
IsGroupEmpty
ObjectGroups
ObjectsByGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
object_ids = rhutil.coerceguidlist(object_ids)
if index<0 or not object_ids: return 0
if not scriptcontext.doc.Groups.AddToGroup(index, object_ids): return 0
return len(object_ids)
def AddObjectToGroup(object_id, group_name):
"""Adds a single object to an existing group.
Parameters:
object_id (guid): String or Guid representing the object identifier
group_name (str): the name of an existing group
Returns:
True or False representing success or failure
Example:
import rhinoscriptsyntax as rs
name = "NewGroup"
id = rs.GetObject("Select object to add to group")
if id: rs.AddObjectToGroup(id,name)
See Also:
AddObjectsToGroup
IsGroupEmpty
ObjectGroups
ObjectsByGroup
"""
object_id = rhutil.coerceguid(object_id)
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
if object_id is None or index<0: return False
return scriptcontext.doc.Groups.AddToGroup(index, object_id)
def DeleteGroup(group_name):
"""Removes an existing group from the document. Reference groups cannot be
removed. Deleting a group does not delete the member objects
Parameters:
group_name (str): the name of an existing group
Returns:
bool: True or False representing success or failure
Example:
import rhinoscriptsyntax as rs
groups = rs.GroupNames()
if groups:
for group in groups: rs.DeleteGroup(group)
See Also:
AddGroup
GroupCount
GroupNames
IsGroup
RenameGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
return scriptcontext.doc.Groups.Delete(index)
def GroupCount():
"""Returns the number of groups in the document
Returns:
number: the number of groups in the document
Example:
import rhinoscriptsyntax as rs
numgroups = rs.GroupCount()
print("Group count:{}".format(numgroups))
See Also:
AddGroup
DeleteGroup
GroupNames
IsGroup
RenameGroup
"""
return scriptcontext.doc.Groups.Count
def GroupNames():
"""Returns the names of all the groups in the document
None if no names exist in the document
Returns:
list(str, ...): the names of all the groups in the document. None if no names exist in the document
Example:
import rhinoscriptsyntax as rs
groups = rs.GroupNames()
if groups:
for group in groups: print(group)
See Also:
AddGroup
DeleteGroup
GroupCount
IsGroup
RenameGroup
"""
names = scriptcontext.doc.Groups.GroupNames(True)
if names is None: return None
return list(names)
def HideGroup(group_name):
"""Hides a group of objects. Hidden objects are not visible, cannot be
snapped to, and cannot be selected
Parameters:
group_name (str): the name of an existing group
Returns:
number: The number of objects that were hidden
Example:
import rhinoscriptsyntax as rs
groups = rs.GroupNames()
if groups:
for group in groups: rs.HideGroup(group)
See Also:
LockGroup
ShowGroup
UnlockGroup
"""
index = scriptcontext.doc.Groups.Find(group_name)
if index<0: return 0
return scriptcontext.doc.Groups.Hide(index);
def IsGroup(group_name):
"""Verifies the existance of a group
Parameters:
group_name (str): the name of the group to check for
Returns:
bool: True or False
Example:
import rhinoscriptsyntax as rs
group = rs.GetString("Group name to verify")
if rs.IsGroup(group):
print("The group exists.")
else:
print("The group does not exist.")
See Also:
AddGroup
DeleteGroup
GroupCount
GroupNames
RenameGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
return scriptcontext.doc.Groups.Find(group_name)>=0
def IsGroupEmpty(group_name):
"""Verifies that an existing group is empty, or contains no object members
Parameters:
group_name (str): the name of an existing group
Returns:
bool: True or False if group_name exists
None: if group_name does not exist
Example:
import rhinoscriptsyntax as rs
names = rs.GroupNames()
if names:
for name in names:
if rs.IsGroupEmpty(name): rs.DeleteGroup(name)
See Also:
AddObjectsToGroup
AddObjectToGroup
RemoveObjectFromAllGroups
RemoveObjectFromGroup
RemoveObjectsFromGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
if index<0: return scriptcontext.errorhandler()
return scriptcontext.doc.Groups.GroupObjectCount(index)>0
def LockGroup(group_name):
"""Locks a group of objects. Locked objects are visible and they can be
snapped to. But, they cannot be selected
Parameters:
group_name (str): the name of an existing group
Returns:
number: Number of objects that were locked if successful
None: on error
Example:
import rhinoscriptsyntax as rs
names = rs.GroupNames()
if names:
for name in names: rs.LockGroup(name)
See Also:
HideGroup
ShowGroup
UnlockGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
if index<0: return scriptcontext.errorhandler()
return scriptcontext.doc.Groups.Lock(index);
def RemoveObjectFromAllGroups(object_id):
"""Removes a single object from any and all groups that it is a member.
Neither the object nor the group can be reference objects
Parameters:
object_id (guid): the object identifier
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
object = rs.GetObject("Select object")
if object: rs.RemoveObjectFromAllGroups(object)
See Also:
IsGroupEmpty
ObjectGroups
ObjectsByGroup
RemoveObjectFromGroup
RemoveObjectsFromGroup
"""
rhinoobject = rhutil.coercerhinoobject(object_id, True, True)
if rhinoobject.GroupCount<1: return False
attrs = rhinoobject.Attributes
attrs.RemoveFromAllGroups()
return scriptcontext.doc.Objects.ModifyAttributes(rhinoobject, attrs, True)
def RemoveObjectFromGroup(object_id, group_name):
"""Remove a single object from an existing group
Parameters:
object_id (guid): the object identifier
group_name (str): the name of an existing group
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
name = "NewGroup"
id = rs.GetObject("Select object")
if name: rs.RemoveObjectFromGroup(id,name)
See Also:
IsGroupEmpty
ObjectGroups
ObjectsByGroup
RemoveObjectFromAllGroups
RemoveObjectsFromGroup
"""
count = RemoveObjectsFromGroup(object_id, group_name)
return not (count is None or count<1)
def RemoveObjectsFromGroup(object_ids, group_name):
"""Removes one or more objects from an existing group
Parameters:
object_ids ([guid, ...]): a list of object identifiers
group_name (str): the name of an existing group
Returns:
number: The number of objects removed from the group is successful
None: on error
Example:
import rhinoscriptsyntax as rs
group = "NewGroup"
ids = rs.GetObjects("Select objects")
if ids: rs.RemoveObjectsFromGroup(ids,group)
See Also:
IsGroupEmpty
ObjectGroups
ObjectsByGroup
RemoveObjectFromAllGroups
RemoveObjectFromGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
if index<0: return scriptcontext.errorhandler()
id = rhutil.coerceguid(object_ids, False)
if id: object_ids = [id]
objects_removed = 0
for id in object_ids:
rhinoobject = rhutil.coercerhinoobject(id, True, True)
attrs = rhinoobject.Attributes
attrs.RemoveFromGroup(index)
if scriptcontext.doc.Objects.ModifyAttributes(rhinoobject, attrs, True):
objects_removed+=1
return objects_removed
def RenameGroup(old_name, new_name):
"""Renames an existing group
Parameters:
old_name (str): the name of an existing group
new_name (str): the new group name
Returns:
str: the new group name if successful
None: on error
Example:
import rhinoscriptsyntax as rs
strOldGroup = rs.GetString("Old group name")
if strOldGroup:
strNewGroup = rs.GetString("New group name")
if strNewName: rs.RenameGroup(strOldGroup, strNewGroup)
See Also:
AddGroup
DeleteGroup
GroupCount
GroupNames
IsGroup
"""
if not isinstance(old_name, str): old_name = str(old_name)
index = scriptcontext.doc.Groups.Find(old_name)
if index<0: return scriptcontext.errorhandler()
if not isinstance(new_name, str): new_name = str(new_name)
if scriptcontext.doc.Groups.ChangeGroupName(index, new_name):
return new_name
return scriptcontext.errorhandler()
def ShowGroup(group_name):
"""Shows a group of previously hidden objects. Hidden objects are not
visible, cannot be snapped to, and cannot be selected
Parameters:
group_name (str): the name of an existing group
Returns:
number: The number of objects that were shown if successful
None: on error
Example:
import rhinoscriptsyntax as rs
groups = rs.GroupNames()
if groups:
for group in groups: rs.ShowGroup(group)
See Also:
HideGroup
LockGroup
UnlockGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
if index<0: return scriptcontext.errorhandler()
return scriptcontext.doc.Groups.Show(index);
def UnlockGroup(group_name):
"""Unlocks a group of previously locked objects. Lockes objects are visible,
can be snapped to, but cannot be selected
Parameters:
group_name (str): the name of an existing group
Returns:
number: The number of objects that were unlocked if successful
None: on error
Example:
import rhinoscriptsyntax as rs
groups = rs.GroupNames()
if groups:
for group in groups: rs.UnlockGroup(group)
See Also:
HideGroup
LockGroup
ShowGroup
"""
if not isinstance(group_name, str): group_name = str(group_name)
index = scriptcontext.doc.Groups.Find(group_name)
if index<0: return scriptcontext.errorhandler()
return scriptcontext.doc.Groups.Unlock(index);
def ObjectTopGroup(object_id):
"""Returns the top most group name that an object is assigned. This function primarily applies to objects that are members of nested groups
Parameters:
object_id (guid): String or Guid representing the object identifier
Returns:
str: the top group's name if successful
None: if not successful
Example:
import rhinoscriptsyntax as rs
id = rs.GetObject("Select object to add to group")
groupName = rs.ObjectTopGroup(id)
See Also:
ObjectGroups
"""
object_id = rhutil.coerceguid(object_id)
if object_id is None: return None
obj = scriptcontext.doc.Objects.FindId(object_id)
topGroupName = None
groupIndexes = obj.GetGroupList()
if groupIndexes is not None:
topGroupIndex = max(groupIndexes) # this is a bad assumption. See RH-49189
topGroupName = scriptcontext.doc.Groups.FindIndex(topGroupIndex).Name
return topGroupName
```
--------------------------------------------------------------------------------
/rhino_mcp_server/static/grips.py:
--------------------------------------------------------------------------------
```python
import Rhino
import scriptcontext
import rhinocompat as compat
from rhinoscript import utility as rhutil
def __neighborgrip(i, object_id, index, direction, enable):
rhobj = rhutil.coercerhinoobject(object_id, True, True)
grips = rhobj.GetGrips()
if not grips or len(grips)<=index: return scriptcontext.errorhandler()
grip = grips[index]
next_grip=None
if direction==0:
next_grip = grip.NeighborGrip(i,0,0,False)
else:
next_grip = grip.NeighborGrip(0,i,0,False)
if next_grip and enable:
next_grip.Select(True)
scriptcontext.doc.Views.Redraw()
return next_grip
def EnableObjectGrips(object_id, enable=True):
"""Enables or disables an object's grips. For curves and surfaces, these are
also called control points.
Parameters:
object_id (guid): identifier of the object
enable (bool, optional): if True, the specified object's grips will be turned on.
Otherwise, they will be turned off
Returns:
bool: True on success, False on failure
Example:
import rhinoscriptsyntax as rs
objects = rs.GetObjects("Select objects")
if objects: [rs.EnableObjectGrips(obj) for obj in objs]
See Also:
ObjectGripCount
ObjectGripsOn
ObjectGripsSelected
SelectObjectGrips
UnselectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if enable!=rhobj.GripsOn:
rhobj.GripsOn = enable
scriptcontext.doc.Views.Redraw()
def GetObjectGrip(message=None, preselect=False, select=False):
"""Prompts the user to pick a single object grip
Parameters:
message (str, optional): prompt for picking
preselect (bool, optional): allow for selection of pre-selected object grip.
select (bool, optional): select the picked object grip.
Returns:
tuple(guid, number, point): defining a grip record.
[0] = identifier of the object that owns the grip
[1] = index value of the grip
[2] = location of the grip
None: on error
Example:
import rhinoscriptsyntax as rs
curve = rs.GetObject("Select a curve", rs.filter.curve)
if curve:
rs.EnableObjectGrips( curve )
grip = rs.GetObjectGrip("Select a curve grip")
if grip: print(grip[2])
See Also:
GetObjectGrips
"""
if not preselect:
scriptcontext.doc.Objects.UnselectAll()
scriptcontext.doc.Views.Redraw()
rc, grip = Rhino.Input.RhinoGet.GetGrip(message)
if rc!=Rhino.Commands.Result.Success: return scriptcontext.errorhandler()
if select:
grip.Select(True, True)
scriptcontext.doc.Views.Redraw()
return grip.OwnerId, grip.Index, grip.CurrentLocation
def GetObjectGrips(message=None, preselect=False, select=False):
"""Prompts user to pick one or more object grips from one or more objects.
Parameters:
message (str, optional): prompt for picking
preselect (bool, optional): allow for selection of pre-selected object grips
select (bool, optional) select the picked object grips
Returns:
list((guid, number, point), ...): containing one or more grip records. Each grip record is a tuple
[n][0] = identifier of the object that owns the grip
[n][1] = index value of the grip
[n][2] = location of the grip
None: on error
Example:
import rhinoscriptsyntax as rs
curves = rs.GetObjects("Select curves", rs.filter.curves)
if curves:
for curve in curves: rs.EnableObjectGrips(curve)
grips = rs.GetObjectGrips("Select curve grips")
if grips: for grip in grips: print(grip[0])
See Also:
GetObjectGrip
"""
if not preselect:
scriptcontext.doc.Objects.UnselectAll()
scriptcontext.doc.Views.Redraw()
getrc, grips = Rhino.Input.RhinoGet.GetGrips(message)
if getrc!=Rhino.Commands.Result.Success or not grips:
return scriptcontext.errorhandler()
rc = []
for grip in grips:
id = grip.OwnerId
index = grip.Index
location = grip.CurrentLocation
rc.append((id, index, location))
if select: grip.Select(True, True)
if select: scriptcontext.doc.Views.Redraw()
return rc
def NextObjectGrip(object_id, index, direction=0, enable=True):
"""Returns the next grip index from a specified grip index of an object
Parameters:
object_id (guid): identifier of the object
index (number): zero based grip index from which to get the next grip index
direction ([number, number], optional): direction to get the next grip index (0=U, 1=V)
enable (bool, optional): if True, the next grip index found will be selected
Returns:
number: index of the next grip on success
None: on failure
Example:
import rhinoscriptsyntax as rs
object_id = rs.GetObject("Select curve", rs.filter.curve)
if object_id:
rs.EnableObjectGrips( object_id )
count = rs.ObjectGripCount( object_id )
for i in range(0,count,2):
rs.NextObjectGrip(object_id, i, 0, True)
See Also:
EnableObjectGrips
PrevObjectGrip
"""
return __neighborgrip(1, object_id, index, direction, enable)
def ObjectGripCount(object_id):
"""Returns number of grips owned by an object
Parameters:
object_id (guid): identifier of the object
Returns:
number: number of grips if successful
None: on error
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if rs.ObjectGripsOn(obj):
print("Grip count ={}".format(rs.ObjectGripCount(obj)))
See Also:
EnableObjectGrips
ObjectGripsOn
ObjectGripsSelected
SelectObjectGrips
UnselectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
grips = rhobj.GetGrips()
if not grips: return scriptcontext.errorhandler()
return grips.Length
def ObjectGripLocation(object_id, index, point=None):
"""Returns or modifies the location of an object's grip
Parameters:
object_id (guid) identifier of the object
index (number): index of the grip to either query or modify
point (point, optional): 3D point defining new location of the grip
Returns:
point: if point is not specified, the current location of the grip referenced by index
point: if point is specified, the previous location of the grip referenced by index
None: on error
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select curve", rs.filter.curve)
if obj:
rs.EnableObjectGrips(obj)
point = rs.ObjectGripLocation(obj, 0)
point[0] = point[0] + 1.0
point[1] = point[1] + 1.0
point[2] = point[2] + 1.0
rs.ObjectGripLocation(obj, 0, point)
rs.EnableObjectGrips(obj, False)
See Also:
EnableObjectGrips
ObjectGripLocations
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return scriptcontext.errorhandler()
grips = rhobj.GetGrips()
if not grips or index<0 or index>=grips.Length:
return scriptcontext.errorhandler()
grip = grips[index]
rc = grip.CurrentLocation
if point:
grip.CurrentLocation = rhutil.coerce3dpoint(point, True)
scriptcontext.doc.Objects.GripUpdate(rhobj, True)
scriptcontext.doc.Views.Redraw()
return rc
def ObjectGripLocations(object_id, points=None):
"""Returns or modifies the location of all grips owned by an object. The
locations of the grips are returned in a list of Point3d with each position
in the list corresponding to that grip's index. To modify the locations of
the grips, you must provide a list of points that contain the same number
of points at grips
Parameters:
object_id (guid): identifier of the object
points ([point, ...], optional) list of 3D points identifying the new grip locations
Returns:
list(point, ...): if points is not specified, the current location of all grips
list(point, ...): if points is specified, the previous location of all grips
None: if not successful
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select curve", rs.filter.curve)
if obj:
rs.EnableObjectGrips( obj )
points = rs.ObjectGripLocations(obj)
for point in points: print(point)
See Also:
EnableObjectGrips
ObjectGripCount
ObjectGripLocation
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return scriptcontext.errorhandler()
grips = rhobj.GetGrips()
if grips is None: return scriptcontext.errorhandler()
rc = [grip.CurrentLocation for grip in grips]
if points and len(points)==len(grips):
points = rhutil.coerce3dpointlist(points, True)
for i, grip in enumerate(grips):
point = points[i]
grip.CurrentLocation = point
scriptcontext.doc.Objects.GripUpdate(rhobj, True)
scriptcontext.doc.Views.Redraw()
return rc
def ObjectGripsOn(object_id):
"""Verifies that an object's grips are turned on
Parameters:
object_id (guid): identifier of the object
Returns:
bool: True or False indicating Grips state
None: on error
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if rs.ObjectGripsOn(obj):
print("Grip count = {}".format(rs.ObjectGripCount(obj)))
See Also:
EnableObjectGrips
ObjectGripCount
ObjectGripsSelected
SelectObjectGrips
UnselectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
return rhobj.GripsOn
def ObjectGripsSelected(object_id):
"""Verifies that an object's grips are turned on and at least one grip
is selected
Parameters:
object_id (guid): identifier of the object
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if rs.ObjectGripsSelected(obj):
rs.UnselectObjectGrips( obj )
See Also:
EnableObjectGrips
ObjectGripCount
ObjectGripsOn
SelectObjectGrips
UnselectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return False
grips = rhobj.GetGrips()
if grips is None: return False
for grip in grips:
if grip.IsSelected(False): return True
return False
def PrevObjectGrip(object_id, index, direction=0, enable=True):
"""Returns the previous grip index from a specified grip index of an object
Parameters:
object_id (guid): identifier of the object
index (number): zero based grip index from which to get the previous grip index
direction ([number, number], optional): direction to get the next grip index (0=U, 1=V)
enable (bool, optional): if True, the next grip index found will be selected
Returns:
number: index of the next grip on success
None: on failure
Example:
import rhinoscriptsyntax as rs
object_id = rs.GetObject("Select curve", rs.filter.curve)
if object_id:
rs.EnableObjectGrips(object_id)
count = rs.ObjectGripCount(object_id)
for i in range(count-1, 0, -2):
rs.PrevObjectGrip(object_id, i, 0, True)
See Also:
EnableObjectGrips
NextObjectGrip
"""
return __neighborgrip(-1, object_id, index, direction, enable)
def SelectedObjectGrips(object_id):
"""Returns a list of grip indices indentifying an object's selected grips
Parameters:
object_id (guid): identifier of the object
Returns:
list(number): list of indices on success
None: on failure or if no grips are selected
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select curve", rs.filter.curve)
if obj:
rs.EnableObjectGrips( obj )
count = rs.ObjectGripCount( obj )
for i in range(0,count,2):
rs.SelectObjectGrip( obj, i )
grips = rs.SelectedObjectGrips(obj)
if grips: print(len(grips{}).format("grips selected"))
See Also:
EnableObjectGrips
SelectObjectGrip
SelectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return None
grips = rhobj.GetGrips()
rc = []
if grips:
for i in compat.RANGE(grips.Length):
if grips[i].IsSelected(False): rc.append(i)
return rc
def SelectObjectGrip(object_id, index):
"""Selects a single grip owned by an object. If the object's grips are
not turned on, the grips will not be selected
Parameters:
object_id (guid) identifier of the object
index (number): index of the grip to select
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select curve", rs.filter.curve)
if obj:
rs.EnableObjectGrips( obj )
count = rs.ObjectGripCount( obj )
for i in range(0,count,2): rs.SelectObjectGrip(obj,i)
See Also:
EnableObjectGrips
ObjectGripCount
SelectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return False
grips = rhobj.GetGrips()
if grips is None: return False
if index<0 or index>=grips.Length: return False
grip = grips[index]
if grip.Select(True,True)>0:
scriptcontext.doc.Views.Redraw()
return True
return False
def SelectObjectGrips(object_id):
"""Selects an object's grips. If the object's grips are not turned on,
they will not be selected
Parameters:
object_id (guid): identifier of the object
Returns:
number: Number of grips selected on success
None: on failure
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if rs.ObjectGripsSelected(obj)==False:
rs.SelectObjectGrips( obj )
See Also:
EnableObjectGrips
ObjectGripCount
SelectObjectGrip
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return scriptcontext.errorhandler()
grips = rhobj.GetGrips()
if grips is None: return scriptcontext.errorhandler()
count = 0
for grip in grips:
if grip.Select(True,True)>0: count+=1
if count>0:
scriptcontext.doc.Views.Redraw()
return count
return scriptcontext.errorhandler()
def UnselectObjectGrip(object_id, index):
"""Unselects a single grip owned by an object. If the object's grips are
not turned on, the grips will not be unselected
Parameters:
object_id (guid): identifier of the object
index (number): index of the grip to unselect
Returns:
bool: True or False indicating success or failure
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select curve", rs.filter.curve)
if obj:
rs.EnableObjectGrips( obj )
count = rs.ObjectGripCount(obj)
for i in range(0,count,2):
rs.UnselectObjectGrip( obj, i )
See Also:
EnableObjectGrips
ObjectGripCount
UnselectObjectGrips
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return False
grips = rhobj.GetGrips()
if grips is None: return False
if index<0 or index>=grips.Length: return False
grip = grips[index]
if grip.Select(False)==0:
scriptcontext.doc.Views.Redraw()
return True
return False
def UnselectObjectGrips(object_id):
"""Unselects an object's grips. Note, the grips will not be turned off.
Parameters:
object_id (guid): identifier of the object
Returns:
number: Number of grips unselected on success
None: on failure
Example:
import rhinoscriptsyntax as rs
obj = rs.GetObject("Select object")
if rs.ObjectGripsSelected(obj): rs.UnselectObjectGrips(obj)
See Also:
EnableObjectGrips
ObjectGripCount
UnselectObjectGrip
"""
rhobj = rhutil.coercerhinoobject(object_id, True, True)
if not rhobj.GripsOn: return scriptcontext.errorhandler()
grips = rhobj.GetGrips()
if grips is None: return scriptcontext.errorhandler()
count = 0
for grip in grips:
if grip.Select(False)==0: count += 1
if count>0:
scriptcontext.doc.Views.Redraw()
return count
return scriptcontext.errorhandler()
```