So I worked with Claude to create a component that can “forge” script components based on raw source code input that includes a header of information like parameter type hints, name, icon, and such. No MCP required. The plugin and a GH definition of examples is attached. Note that it is experimental and likely buggy.
In this incarnation, the header isn’t a standard format like XML, but it is pretty straightforward to read and for an LLM to generate as part of the script.
The Script Forge plugin was compiled on macOS, so I’m not 100% sure it will run on Windows. If anyone tries it, please let me how it runs for you.
Script Forge Documentation
Script Forge
Script Forge is a Grasshopper component that builds other script components from plain text: feed it C# or Python 3 source and press a button, and it creates the component on the canvas — params, type hints, tooltips, name, and icon included. All that metadata comes from an optional @component comment header at the top of the source; without one, the code is simply injected into a stock script component. Point it at the same component again (via the Target input or the header’s guid: field) and it updates in place — wires stay on matching params, and nothing is ever duplicated when the source has not changed. Feed Source a tree — one branch per script — and a single forge fans out to one component per branch, each branch tracked, updated, and reported independently.
/* @component
name: Remap Number
description: Remaps a number from one interval into another.
@in Value | double | item | The number to remap.
@in Source | Interval | item | The interval the value lives in.
@in Target | Interval | item | The interval to map into.
@out Mapped | double | item | The remapped number.
@end
*/
Fully standalone — stock Grasshopper + Rhino 8, no plugins.
Canvas usage
| Param |
Type |
Access |
Meaning |
Source (in) |
string |
tree |
The source text, one branch per script — each branch a list of lines (Read File output) or one multiline string. A plain list or single string is simply the one-branch case. |
Target (in) |
any |
tree |
Optional component(s) to update: Guids, guid strings, or component references, paired with Source branches (see Forging several scripts at once). Falls back to each header’s guid: field; if both are absent a new component is created next to the forge. |
Run (in) |
bool |
item |
Works with a toggle or a momentary button. While true (or on each press), forges every branch whose source text or target changed since the last run. |
ComponentId (out) |
Guid |
tree |
Instance guid of the created/updated component, one branch per Source branch. |
Log (out) |
string |
tree |
Step-by-step report per Source branch: parse, sync, lost wires, compile result, stamping, warnings. |
Typical rig: Panel (file path) → Read File → Source, Button (or Boolean Toggle) → Run, and optionally a Panel (guid) → Target. The source can just as well come from a multiline panel with the whole script pasted in — anything that produces text works.
Getting a component’s guid (to update it in place):
- easiest: the forge’s own
ComponentId output is the guid of whatever it just forged — wire it to a panel and copy from there;
- put a
guid: field in the header instead — then the source itself pins its component and no Target wire is ever needed;
- or use any component-info tool (e.g. Metahopper’s Object Info) — the Target input also accepts component reference goo directly, not just guid text.
Semantics worth knowing
- Wires survive updates by param name first, then by position. Params whose names are unchanged keep their
IGH_Param objects, so their wires stay. A param that was renamed in place is recycled positionally — the Log reports renamed X -> Y (n wire(s) kept) — so index-style renames keep their wires too. Only params that are truly removed lose wires, and each loss is reported.
- Params end up in exactly the header’s order, so script bodies (or downstream tooling) that address params by index stay valid — the header is the index order.
- Idempotent, button-friendly. The forge records what it last applied (in the document’s value table, saved inside the
.gh), so recomputes, reopening the file, recompiling the forge, and repeated button presses do not create duplicates. It re-forges only when the source text or target changed, or when the previously forged component was deleted. To force a rebuild: edit the source, or delete the forged component.
- While a toggle holds Run true, every input change forges immediately. Toggle Run off before re-pointing the source/target panels, or the transient in-between states will forge stray components. (A button avoids this by construction.)
- A hidden
out pin is fine. The stdout param is recognized by its type, never by name or index, so components whose standard output param is hidden (right-click → Standard Output/Error Parameter) update cleanly and stay hidden.
- The languages must match on update: pushing a Python source at a C# Script component (or vice versa) is refused with an error.
Forging several scripts at once
Source is a tree: each branch is one script. Two panels through Entwine, a grafted list of multiline strings, several Read File outputs merged into branches — any tree shape works, and the single-script rigs above are just the one-branch case. All branches forge in one pass; ComponentId and Log come back as trees with matching paths.
Pairing targets. Each Source branch finds its Target as follows:
- a Target branch with the identical path → its first item;
- otherwise, if Target is a single flat list, the item at the branch’s position (first branch ↔ first item, and so on);
- otherwise (no match, or a null/empty item): the header’s
guid: field, or a new component.
A null or empty entry in a Target list therefore means “forge a new component for this one” — so one list can mix updates and creations. The same component may not be targeted by two branches; the second claim errors (that branch only) and is skipped.
Per-branch state. The applied-source key and result guid are tracked per (forge instance, branch path), so re-running with one branch edited re-forges only that branch. The create/update contract, stated plainly:
- no target → a new component per new
(branch, source);
- same source and target as last run → no-op;
- target set (input or header
guid:) → update in place.
One consequence: state follows the branch path, so reordering the branches of unpinned sources re-forges them as new components. Pin scripts via a Target or a header guid: when identity must survive reshuffles. Removing a branch leaves its forged component on canvas untouched; the branch’s saved state is pruned from the document.
The @component header
The header is the first comment block of the source. It is optional — see Headerless sources below.
/* @component
name: Curve Frames
nickname: Frames
category: Curve
subcategory: Division
icon: icons/curve-frames.svg
description: Divides a curve into equal-parameter segments and returns a frame
at each division point. Long values wrap onto indented follow-on lines.
@in Path | Curve | item | The curve to divide.
@in Count | int | item | Number of segments.
@out Planes | Plane | list | One frame per division point.
@out Parameters | double | list | The curve parameter at each division point.
@end
*/
In Python use a module docstring or hash comments — same grammar, the parser strips the comment prefix per line:
"""@component
name: Point Grid PY
...
@end
"""
# @component
# name: Curve Divide PY
# ...
# @end
Fields
One per line, key: value. Unrecognized keys are ignored (forward-compatible).
| Key |
Required |
Meaning |
name |
yes |
Component Name (shown in tooltips and menus). |
description |
yes |
The hover tooltip. No double quotes — see Warnings. |
nickname |
no |
Canvas nickname. Defaults to name. |
category / subcategory |
no |
Ribbon tab / panel. Informational for inline components. |
icon |
no |
Path to an SVG or PNG icon, relative to the folder of the saved .gh. See Icons. |
language |
no |
csharp or python. Usually unnecessary — see Language detection. |
guid |
no |
Pins the target component instance. With a pinned guid the forge always updates that component (or recreates it with that exact guid), without needing the Target input. |
marker |
no |
A literal tag string preserved verbatim in the source (for components discovered by source-text scans). May repeat. |
Continuation lines
Any non-blank line that is not itself a key: value, @in/@out, or @end line continues the preceding value, joined with a single space. Indent continuations for readability. A blank line ends the continuation. Caveat: a continuation line that looks like a field (Options: a, b) will be parsed as a new field — rephrase, or keep the colon away from the line start.
Param lines
@in Name | typehint | access | description
@out Name | typehint | access | description
- Four fields split on the first three
| only — the description may freely contain |, :, commas, and non-ASCII.
Name must match the RunScript argument name (C#). PascalCase by convention.
access is item (single value), list (one branch), or tree (multi-branch). In the C# signature: T ⇒ item, List<T> ⇒ list, DataTree<T> ⇒ tree.
- Output hints are decorative in GH (outputs are typed by what you assign), but recording them keeps the header a complete spec.
Type hints
The typehint field selects the param’s Converter (right-click → Type Hint on a live param). Matching is case-insensitive; the same vocabulary works for C# and Python components. The full set on Rhino 8:
| Hint |
Notes |
bool, int, double, string |
primitives (double selects the converter Python names float; both spellings accepted) |
object (or none, or blank) |
No Type Hint — raw goo, no conversion |
Guid, DateTime, Complex, Color |
.NET / GH types |
Point3d (point), Vector3d (vector), Plane (plane) |
frames & vectors |
Interval, UVInterval |
GH Domain and Domain² |
Line, Circle, Arc, Polyline, Rectangle3d |
primitive curves |
Curve (curve) |
any curve |
Box, Transform, Point3dList |
|
Mesh (mesh), Surface, Extrusion, SubD, Brep (brep) |
surfaces & solids |
PointCloud, GeometryBase |
GeometryBase accepts any geometry |
Hatch, TextDot, TextEntity, Leader |
annotations |
An unknown hint falls back to No Type Hint and the forge reports a HINT WARNING in the Log — hint typos never fail silently.
Python outputs are never hinted. On Python components an output type hint is an active converter applied to the assigned object wholesale, which breaks list outputs (PyObject to Point3d conversion errors). The forge leaves Python outputs on No Type Hint — matching stock components — and the header’s @out hints serve as documentation. (C# outputs are write-only object sinks, so their hints are applied but harmless either way.) The source push (set_Text) has a side effect of clearing a Python component’s Marshal Inputs / Outputs / Guids toggles, which would leave a returned list wrapped as one opaque PyObject — so the forge captures the three toggles before every push and restores them right after: a fresh component keeps the stock defaults (all on, lists unwrap into proper GH lists), an update keeps whatever the user has chosen.
Icons
icon: accepts three forms:
-
an SVG path — rasterized to a 24×24 PNG automatically (via macOS sips) next to the SVG; the PNG is reused until the SVG is newer;
-
a PNG path — used directly (make it 24×24; GH draws stamped icons at native pixel size);
-
an embedded base64 PNG — base64:<payload> (or a full data:image/png;base64, URI), so the icon travels inside the source with no file on disk. Long payloads may wrap across indented continuation lines; the joining whitespace is ignored by the decoder:
icon: base64:iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNS
R0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAA...
Path resolution: an absolute path is used as-is. A relative path is resolved against the folder of the saved .gh document — because the forge receives plain text, it cannot know where the source file lives (or whether it ever was a file), so the document’s own folder is the only stable anchor. Keep icons next to the .gh (e.g. an icons/ subfolder), or use an absolute path or a base64 payload when that doesn’t suit. If the document has never been saved, relative icons are skipped with a warning saying so. Keep icons square and legible at 24 px.
The icon step is strictly best-effort on every platform: a missing file, an unsupported extension, a corrupt image, an undecodable payload, or SVG rasterization off macOS each produce a clear icon warning: … line in the Log and the forge carries on — params, source, and the rest of the metadata are never affected. On Windows, use a PNG path or a base64 icon (or ship a pre-rasterized <name>.png next to the SVG); SVG-only icons are skipped there with a warning saying exactly that.
Language detection
Priority order:
- header
language: field (csharp / python);
- a
#! python shebang in the first lines;
- the header comment style —
/* @component ⇒ C#, """@component or # @component ⇒ Python;
- for headerless updates, the target component’s own type;
- headerless heuristics —
Script_Instance or using X; ⇒ C#; a leading docstring or import ⇒ Python.
If none of these decide, the forge errors and asks for a language: field.
Headerless sources
A source with no @component header is injected as-is: the forge creates a stock script component (default x, y inputs and out, a outputs) or pushes into the existing target, touching no params, metadata, or icon — exactly like dropping a default component and pasting the code in. The stock default C# and Python scripts work unchanged.
Warnings
The forge validates at forge time and reports problems in the Log (they never block the forge):
DRIFT WARNING — a header @in/@out name is missing from the C# RunScript signature, or vice versa. Drift is otherwise invisible: the canvas hints silently win over the signature at solve time.
QUOTE WARNING — a description contains a double quote. Fine on canvas, but it breaks the Rhino ScriptEditor plugin builder if the component is ever published (descriptions are embedded as unescaped C# string literals).
HINT WARNING — a type hint name was not recognized; the param got No Type Hint.
lost wire: … — a renamed/removed param dropped its connections.
Requirements and limits
- Rhino 8 — targets the modern
C# Script / Python 3 Script components (RhinoCodePluginGH). Legacy GHPython and the old C# script component are not supported as targets.
- SVG icon rasterization uses macOS
sips. On Windows, point icon: at a 24×24 PNG, embed it as base64:<payload>, or ship a pre-rasterized <name>.png next to the SVG. Icon problems of any kind only warn and never block the forge. Everything else is cross-platform.
- One forge handles any number of scripts via Source branches, each with its own state (keyed by instance guid + branch path). Extra forge components also coexist cleanly — and yes, the forge can forge more forges.
- The Log shows the previous run’s report until a new forge completes — the forge finishes a couple of solutions after the press (schedule → mutate → compile → stamp), which is normally imperceptible.