lamcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lamcp-0.1.0/.github/workflows/build.yml +57 -0
- lamcp-0.1.0/.github/workflows/release.yml +98 -0
- lamcp-0.1.0/.gitignore +32 -0
- lamcp-0.1.0/CHANGELOG.md +28 -0
- lamcp-0.1.0/LICENSE +21 -0
- lamcp-0.1.0/PKG-INFO +207 -0
- lamcp-0.1.0/README.md +172 -0
- lamcp-0.1.0/RELEASING.md +62 -0
- lamcp-0.1.0/grasshopper/Lamcp_Bridge/code.py +149 -0
- lamcp-0.1.0/grasshopper/Lamcp_Bridge/icon.png +0 -0
- lamcp-0.1.0/grasshopper/Lamcp_Bridge/metadata.json +30 -0
- lamcp-0.1.0/lamcp/__init__.py +1 -0
- lamcp-0.1.0/lamcp/server.py +131 -0
- lamcp-0.1.0/pyproject.toml +28 -0
- lamcp-0.1.0/uv.lock +1724 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
python: ['3.10', '3.11', '3.12', '3.13']
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python }}
|
|
21
|
+
- name: Install
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip
|
|
24
|
+
pip install -e ".[dev]"
|
|
25
|
+
- name: Lint
|
|
26
|
+
run: ruff check .
|
|
27
|
+
- name: Format check
|
|
28
|
+
run: ruff format --check .
|
|
29
|
+
- name: Import smoke test
|
|
30
|
+
run: |
|
|
31
|
+
python -c "from lamcp.server import mcp, run_python_script, bridge_health, unload_python_modules"
|
|
32
|
+
|
|
33
|
+
build-ghpython-component:
|
|
34
|
+
runs-on: windows-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v6
|
|
37
|
+
- uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: '3.11'
|
|
40
|
+
- name: Install componentizer deps
|
|
41
|
+
# The action's `componentize_cpy.py` does `import clr` (Python.NET)
|
|
42
|
+
# but doesn't install pythonnet itself, so we pre-install it here.
|
|
43
|
+
run: pip install pythonnet
|
|
44
|
+
- name: Build Lamcp_Bridge.ghuser
|
|
45
|
+
# `target` must be a single-segment path: the action uses `os.mkdir`
|
|
46
|
+
# (not `makedirs`), so nested paths like `dist/ghuser/` fail because
|
|
47
|
+
# the parent doesn't exist yet.
|
|
48
|
+
uses: compas-dev/compas-actions.ghpython_components@v5
|
|
49
|
+
with:
|
|
50
|
+
source: grasshopper
|
|
51
|
+
target: dist
|
|
52
|
+
interpreter: cpython
|
|
53
|
+
- uses: actions/upload-artifact@v5
|
|
54
|
+
with:
|
|
55
|
+
name: Lamcp_Bridge-ghuser
|
|
56
|
+
path: dist/*.ghuser
|
|
57
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-python:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.11'
|
|
16
|
+
- name: Build sdist + wheel
|
|
17
|
+
run: |
|
|
18
|
+
python -m pip install --upgrade pip build
|
|
19
|
+
python -m build
|
|
20
|
+
- uses: actions/upload-artifact@v5
|
|
21
|
+
with:
|
|
22
|
+
name: python-dist
|
|
23
|
+
path: dist/*
|
|
24
|
+
if-no-files-found: error
|
|
25
|
+
|
|
26
|
+
build-ghpython-component:
|
|
27
|
+
runs-on: windows-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
python-version: '3.11'
|
|
33
|
+
- name: Install componentizer deps
|
|
34
|
+
# The action's `componentize_cpy.py` does `import clr` (Python.NET)
|
|
35
|
+
# but doesn't install pythonnet itself, so we pre-install it here.
|
|
36
|
+
run: pip install pythonnet
|
|
37
|
+
- name: Build Lamcp_Bridge.ghuser
|
|
38
|
+
# `target` must be a single-segment path: the action uses `os.mkdir`
|
|
39
|
+
# (not `makedirs`), so nested paths like `dist/ghuser/` fail because
|
|
40
|
+
# the parent doesn't exist yet.
|
|
41
|
+
uses: compas-dev/compas-actions.ghpython_components@v5
|
|
42
|
+
with:
|
|
43
|
+
source: grasshopper
|
|
44
|
+
target: dist
|
|
45
|
+
interpreter: cpython
|
|
46
|
+
- uses: actions/upload-artifact@v5
|
|
47
|
+
with:
|
|
48
|
+
name: ghpython-dist
|
|
49
|
+
path: dist/*.ghuser
|
|
50
|
+
if-no-files-found: error
|
|
51
|
+
|
|
52
|
+
publish-pypi:
|
|
53
|
+
# Only publish on actual tag pushes, not on workflow_dispatch.
|
|
54
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
55
|
+
needs: build-python
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
environment:
|
|
58
|
+
name: pypi
|
|
59
|
+
url: https://pypi.org/p/lamcp
|
|
60
|
+
permissions:
|
|
61
|
+
id-token: write # required for OIDC trusted publishing
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/download-artifact@v5
|
|
64
|
+
with:
|
|
65
|
+
name: python-dist
|
|
66
|
+
path: dist/
|
|
67
|
+
- name: Publish to PyPI
|
|
68
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
69
|
+
|
|
70
|
+
github-release:
|
|
71
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
72
|
+
needs: [build-python, build-ghpython-component, publish-pypi]
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
permissions:
|
|
75
|
+
contents: write
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/checkout@v6
|
|
78
|
+
- uses: actions/download-artifact@v5
|
|
79
|
+
with:
|
|
80
|
+
name: python-dist
|
|
81
|
+
path: dist/
|
|
82
|
+
- uses: actions/download-artifact@v5
|
|
83
|
+
with:
|
|
84
|
+
name: ghpython-dist
|
|
85
|
+
path: dist/
|
|
86
|
+
- name: Create GitHub Release
|
|
87
|
+
uses: softprops/action-gh-release@v2
|
|
88
|
+
with:
|
|
89
|
+
files: |
|
|
90
|
+
dist/*.whl
|
|
91
|
+
dist/*.tar.gz
|
|
92
|
+
dist/*.ghuser
|
|
93
|
+
generate_release_notes: true
|
|
94
|
+
body: |
|
|
95
|
+
Install the Python MCP server: `pip install lamcp==${{ github.ref_name }}` (drop the leading `v`).
|
|
96
|
+
|
|
97
|
+
For Rhino 8 / Grasshopper users: download `Lamcp_Bridge.ghuser` below and drop it into your Grasshopper Libraries folder
|
|
98
|
+
(Grasshopper menu → File → Special Folders → Components Folder).
|
lamcp-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg
|
|
9
|
+
|
|
10
|
+
# Virtual envs
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
env/
|
|
14
|
+
|
|
15
|
+
# Tooling
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.tox/
|
|
20
|
+
|
|
21
|
+
# Editor / OS
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
*.swp
|
|
25
|
+
.DS_Store
|
|
26
|
+
|
|
27
|
+
# Project-local Claude state
|
|
28
|
+
.claude/settings.local.json
|
|
29
|
+
|
|
30
|
+
# Built Grasshopper user objects (these are release artifacts, not source)
|
|
31
|
+
*.ghuser
|
|
32
|
+
grasshopper/**/__pycache__/
|
lamcp-0.1.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-05-31
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
* Initial release.
|
|
15
|
+
* `lamcp` FastMCP server with three tools:
|
|
16
|
+
* `run_python_script(code, timeout)` — exec arbitrary Python inside the
|
|
17
|
+
bridge process, captures stdout / stderr / `repr(_)` / traceback.
|
|
18
|
+
* `unload_python_modules(prefix)` — drop `sys.modules[prefix.*]` to
|
|
19
|
+
pick up on-disk module edits without restarting Rhino.
|
|
20
|
+
* `bridge_health()` — ping the bridge over loopback HTTP.
|
|
21
|
+
* `LAMCP Bridge` Grasshopper component (Rhino 8, CPython 3.9) that
|
|
22
|
+
hosts an `http.server` on `127.0.0.1:8765` (configurable port) and
|
|
23
|
+
`exec()`s incoming code against a shared globals dict so state
|
|
24
|
+
persists across calls.
|
|
25
|
+
* GitHub release ships the pre-built `Lamcp_Bridge.ghuser` alongside
|
|
26
|
+
the Python wheel/sdist, so users can drop the component into their
|
|
27
|
+
Grasshopper Libraries folder without a manual paste-and-configure
|
|
28
|
+
step.
|
lamcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gramazio Kohler Research, ETH Zürich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lamcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lamcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lambda MCP: teach your LLM to do Grasshopper tricks.
|
|
5
|
+
Author: Gramazio Kohler Research, ETH Zürich
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Gramazio Kohler Research, ETH Zürich
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Keywords: bridge,claude,grasshopper,llm,mcp,rhino
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: fastmcp>=0.4
|
|
31
|
+
Requires-Dist: httpx>=0.27
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# LAMCP
|
|
37
|
+
|
|
38
|
+
**LA**mbda **MCP**: teach your LLM to do Grasshopper tricks.
|
|
39
|
+
|
|
40
|
+
Lets Claude Code (or any MCP client) introspect and mutate a
|
|
41
|
+
live Grasshopper session in real time: inspect the canvas,
|
|
42
|
+
wire components, read/write slider values, run `RhinoCommon`
|
|
43
|
+
calls, hot-reload modules -all from inside an AI agent loop, without rebuilding userobjects or restarting Rhino.
|
|
44
|
+
|
|
45
|
+
## Architecture
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
LLM ──MCP stdio──▶ lamcp (Python 3.10+)
|
|
49
|
+
│
|
|
50
|
+
│ HTTP POST /exec {"code": "...", "timeout": 30}
|
|
51
|
+
▼
|
|
52
|
+
LAMCP Bridge GH component (Rhino 8 CPython 3.9)
|
|
53
|
+
├─ http.server on 127.0.0.1:8765
|
|
54
|
+
├─ exec() with shared globals
|
|
55
|
+
└─ returns {stdout, stderr, result, error}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Why split: Rhino 8's CPython runtime is pinned to 3.9. `fastmcp` and the
|
|
59
|
+
underlying `mcp` SDK require 3.10+. So the MCP-speaking half runs in a
|
|
60
|
+
system Python and forwards over loopback HTTP to a stdlib-only HTTP server
|
|
61
|
+
living inside Rhino as a regular Grasshopper component.
|
|
62
|
+
|
|
63
|
+
## Setup
|
|
64
|
+
|
|
65
|
+
### 1. Install the MCP server
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/gramaziokohler/lamcp.git
|
|
69
|
+
cd lamcp
|
|
70
|
+
uv pip install -e .
|
|
71
|
+
# or: pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Register with Claude Code (or whatever LLM you use)
|
|
75
|
+
|
|
76
|
+
Add to `~/.claude.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"lamcp": {
|
|
82
|
+
"command": "lamcp"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If `lamcp` isn't on `PATH`, use the absolute path to the venv's script:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"lamcp": {
|
|
94
|
+
"command": "/absolute/path/to/.venv/bin/lamcp"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Restart Claude Code so it discovers the new MCP server.
|
|
101
|
+
|
|
102
|
+
### 3. Install the bridge in Grasshopper
|
|
103
|
+
|
|
104
|
+
**Option A — drop the pre-built userobject (recommended).**
|
|
105
|
+
|
|
106
|
+
1. Download `Lamcp_Bridge.ghuser` from the [latest release](https://github.com/gramaziokohler/lamcp/releases/latest).
|
|
107
|
+
2. In Grasshopper: *File → Special Folders → Components Folder*. Move the
|
|
108
|
+
`.ghuser` file there.
|
|
109
|
+
3. Restart Grasshopper. `LAMCP Bridge` appears under the `LAMCP` tab.
|
|
110
|
+
4. Drop it on the canvas, wire a `Boolean Toggle` (set to `True`) into
|
|
111
|
+
`enable`. The `status` output reads `listening on http://127.0.0.1:8765`.
|
|
112
|
+
|
|
113
|
+
**Option B — paste the source manually (for hacking).**
|
|
114
|
+
|
|
115
|
+
1. Drop a Python 3 Script component on the canvas. Paste the contents of
|
|
116
|
+
[`grasshopper/Lamcp_Bridge/code.py`](grasshopper/Lamcp_Bridge/code.py) in.
|
|
117
|
+
2. Add two inputs: `enable` (bool) and `port` (int). Add one output: `status`.
|
|
118
|
+
3. Wire a `Boolean Toggle` (set to `True`) into `enable`.
|
|
119
|
+
4. The `status` output should read `listening on http://127.0.0.1:8765`.
|
|
120
|
+
|
|
121
|
+
Either way, your MCP client now has a `run_python_script` tool that
|
|
122
|
+
exec()s code inside your live Rhino session.
|
|
123
|
+
|
|
124
|
+
## Tools exposed
|
|
125
|
+
|
|
126
|
+
| Tool | Purpose |
|
|
127
|
+
| ----------------------- | ------------------------------------------------------------------------ |
|
|
128
|
+
| `run_python_script` | exec() arbitrary Python inside Rhino, capture stdout / stderr / repr(_) |
|
|
129
|
+
| `unload_python_modules` | drop `sys.modules[prefix.*]` so the next import re-reads from disk |
|
|
130
|
+
| `bridge_health` | ping the bridge to verify it's reachable |
|
|
131
|
+
|
|
132
|
+
### Return contract for `run_python_script`
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"stdout": "...", // captured stdout
|
|
137
|
+
"stderr": "...", // captured stderr
|
|
138
|
+
"result": "repr of _", // assign to `_` to return a value
|
|
139
|
+
"error": null // formatted traceback if exception raised
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Globals persist between calls, so you can `import` once and reuse:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# call 1
|
|
147
|
+
import scriptcontext as sc; doc = sc.doc.ActiveDoc
|
|
148
|
+
# call 2
|
|
149
|
+
print(doc.Name) # `doc` is still bound
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Environment variables
|
|
153
|
+
|
|
154
|
+
| Variable | Default | Purpose |
|
|
155
|
+
| ------------------ | ------------------------ | -------------------------------- |
|
|
156
|
+
| `LAMCP_BRIDGE_URL` | `http://127.0.0.1:8765` | URL of the bridge's HTTP server |
|
|
157
|
+
|
|
158
|
+
## Caveats
|
|
159
|
+
|
|
160
|
+
- **UI thread**: code runs on the HTTP server thread, not the Rhino UI
|
|
161
|
+
thread. Most read-only `RhinoCommon` / `Grasshopper` access works
|
|
162
|
+
cross-thread, but heavy mutations (bulk `RemoveObject`, etc.) can crash
|
|
163
|
+
Rhino. Eto-based UI marshalling is a planned addition.
|
|
164
|
+
- **`isinstance` doesn't always work**: in Rhino 8 CPython, `isinstance`
|
|
165
|
+
against concrete .NET types often returns False due to interface interop.
|
|
166
|
+
Use `obj.GetType().Name == "..."` instead.
|
|
167
|
+
- **`RemoveSource(IGH_Param)` is a silent no-op**: use the
|
|
168
|
+
`RemoveSource(Guid)` overload.
|
|
169
|
+
- **`float(System.Decimal)` raises**: wrap with `System.Convert.ToDouble(x)`
|
|
170
|
+
or `float(str(x))`.
|
|
171
|
+
|
|
172
|
+
## Security
|
|
173
|
+
|
|
174
|
+
The bridge listens on `127.0.0.1` only and accepts no auth: it runs
|
|
175
|
+
arbitrary Python in your Rhino with no sandboxing. **Never expose it
|
|
176
|
+
beyond localhost**, and stop it (`enable=False`) when you're done.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
Install with the `dev` extra to pull in `ruff`:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install -e ".[dev]"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Lint + format checks (same commands CI runs):
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
ruff check . # lint
|
|
190
|
+
ruff format --check . # formatting (non-destructive)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Auto-fix:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
ruff check . --fix # fix lint issues
|
|
197
|
+
ruff format . # reformat
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
For one-off runs without installing into your env, `uvx ruff ...` works
|
|
201
|
+
identically.
|
|
202
|
+
|
|
203
|
+
Releases are tag-driven — see [RELEASING.md](RELEASING.md).
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT
|
lamcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# LAMCP
|
|
2
|
+
|
|
3
|
+
**LA**mbda **MCP**: teach your LLM to do Grasshopper tricks.
|
|
4
|
+
|
|
5
|
+
Lets Claude Code (or any MCP client) introspect and mutate a
|
|
6
|
+
live Grasshopper session in real time: inspect the canvas,
|
|
7
|
+
wire components, read/write slider values, run `RhinoCommon`
|
|
8
|
+
calls, hot-reload modules -all from inside an AI agent loop, without rebuilding userobjects or restarting Rhino.
|
|
9
|
+
|
|
10
|
+
## Architecture
|
|
11
|
+
|
|
12
|
+
```text
|
|
13
|
+
LLM ──MCP stdio──▶ lamcp (Python 3.10+)
|
|
14
|
+
│
|
|
15
|
+
│ HTTP POST /exec {"code": "...", "timeout": 30}
|
|
16
|
+
▼
|
|
17
|
+
LAMCP Bridge GH component (Rhino 8 CPython 3.9)
|
|
18
|
+
├─ http.server on 127.0.0.1:8765
|
|
19
|
+
├─ exec() with shared globals
|
|
20
|
+
└─ returns {stdout, stderr, result, error}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Why split: Rhino 8's CPython runtime is pinned to 3.9. `fastmcp` and the
|
|
24
|
+
underlying `mcp` SDK require 3.10+. So the MCP-speaking half runs in a
|
|
25
|
+
system Python and forwards over loopback HTTP to a stdlib-only HTTP server
|
|
26
|
+
living inside Rhino as a regular Grasshopper component.
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
### 1. Install the MCP server
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/gramaziokohler/lamcp.git
|
|
34
|
+
cd lamcp
|
|
35
|
+
uv pip install -e .
|
|
36
|
+
# or: pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Register with Claude Code (or whatever LLM you use)
|
|
40
|
+
|
|
41
|
+
Add to `~/.claude.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"lamcp": {
|
|
47
|
+
"command": "lamcp"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If `lamcp` isn't on `PATH`, use the absolute path to the venv's script:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"lamcp": {
|
|
59
|
+
"command": "/absolute/path/to/.venv/bin/lamcp"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Restart Claude Code so it discovers the new MCP server.
|
|
66
|
+
|
|
67
|
+
### 3. Install the bridge in Grasshopper
|
|
68
|
+
|
|
69
|
+
**Option A — drop the pre-built userobject (recommended).**
|
|
70
|
+
|
|
71
|
+
1. Download `Lamcp_Bridge.ghuser` from the [latest release](https://github.com/gramaziokohler/lamcp/releases/latest).
|
|
72
|
+
2. In Grasshopper: *File → Special Folders → Components Folder*. Move the
|
|
73
|
+
`.ghuser` file there.
|
|
74
|
+
3. Restart Grasshopper. `LAMCP Bridge` appears under the `LAMCP` tab.
|
|
75
|
+
4. Drop it on the canvas, wire a `Boolean Toggle` (set to `True`) into
|
|
76
|
+
`enable`. The `status` output reads `listening on http://127.0.0.1:8765`.
|
|
77
|
+
|
|
78
|
+
**Option B — paste the source manually (for hacking).**
|
|
79
|
+
|
|
80
|
+
1. Drop a Python 3 Script component on the canvas. Paste the contents of
|
|
81
|
+
[`grasshopper/Lamcp_Bridge/code.py`](grasshopper/Lamcp_Bridge/code.py) in.
|
|
82
|
+
2. Add two inputs: `enable` (bool) and `port` (int). Add one output: `status`.
|
|
83
|
+
3. Wire a `Boolean Toggle` (set to `True`) into `enable`.
|
|
84
|
+
4. The `status` output should read `listening on http://127.0.0.1:8765`.
|
|
85
|
+
|
|
86
|
+
Either way, your MCP client now has a `run_python_script` tool that
|
|
87
|
+
exec()s code inside your live Rhino session.
|
|
88
|
+
|
|
89
|
+
## Tools exposed
|
|
90
|
+
|
|
91
|
+
| Tool | Purpose |
|
|
92
|
+
| ----------------------- | ------------------------------------------------------------------------ |
|
|
93
|
+
| `run_python_script` | exec() arbitrary Python inside Rhino, capture stdout / stderr / repr(_) |
|
|
94
|
+
| `unload_python_modules` | drop `sys.modules[prefix.*]` so the next import re-reads from disk |
|
|
95
|
+
| `bridge_health` | ping the bridge to verify it's reachable |
|
|
96
|
+
|
|
97
|
+
### Return contract for `run_python_script`
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"stdout": "...", // captured stdout
|
|
102
|
+
"stderr": "...", // captured stderr
|
|
103
|
+
"result": "repr of _", // assign to `_` to return a value
|
|
104
|
+
"error": null // formatted traceback if exception raised
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Globals persist between calls, so you can `import` once and reuse:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# call 1
|
|
112
|
+
import scriptcontext as sc; doc = sc.doc.ActiveDoc
|
|
113
|
+
# call 2
|
|
114
|
+
print(doc.Name) # `doc` is still bound
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Environment variables
|
|
118
|
+
|
|
119
|
+
| Variable | Default | Purpose |
|
|
120
|
+
| ------------------ | ------------------------ | -------------------------------- |
|
|
121
|
+
| `LAMCP_BRIDGE_URL` | `http://127.0.0.1:8765` | URL of the bridge's HTTP server |
|
|
122
|
+
|
|
123
|
+
## Caveats
|
|
124
|
+
|
|
125
|
+
- **UI thread**: code runs on the HTTP server thread, not the Rhino UI
|
|
126
|
+
thread. Most read-only `RhinoCommon` / `Grasshopper` access works
|
|
127
|
+
cross-thread, but heavy mutations (bulk `RemoveObject`, etc.) can crash
|
|
128
|
+
Rhino. Eto-based UI marshalling is a planned addition.
|
|
129
|
+
- **`isinstance` doesn't always work**: in Rhino 8 CPython, `isinstance`
|
|
130
|
+
against concrete .NET types often returns False due to interface interop.
|
|
131
|
+
Use `obj.GetType().Name == "..."` instead.
|
|
132
|
+
- **`RemoveSource(IGH_Param)` is a silent no-op**: use the
|
|
133
|
+
`RemoveSource(Guid)` overload.
|
|
134
|
+
- **`float(System.Decimal)` raises**: wrap with `System.Convert.ToDouble(x)`
|
|
135
|
+
or `float(str(x))`.
|
|
136
|
+
|
|
137
|
+
## Security
|
|
138
|
+
|
|
139
|
+
The bridge listens on `127.0.0.1` only and accepts no auth: it runs
|
|
140
|
+
arbitrary Python in your Rhino with no sandboxing. **Never expose it
|
|
141
|
+
beyond localhost**, and stop it (`enable=False`) when you're done.
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
Install with the `dev` extra to pull in `ruff`:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e ".[dev]"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Lint + format checks (same commands CI runs):
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
ruff check . # lint
|
|
155
|
+
ruff format --check . # formatting (non-destructive)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Auto-fix:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
ruff check . --fix # fix lint issues
|
|
162
|
+
ruff format . # reformat
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For one-off runs without installing into your env, `uvx ruff ...` works
|
|
166
|
+
identically.
|
|
167
|
+
|
|
168
|
+
Releases are tag-driven — see [RELEASING.md](RELEASING.md).
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
lamcp-0.1.0/RELEASING.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Releasing LAMCP
|
|
2
|
+
|
|
3
|
+
## One-time setup
|
|
4
|
+
|
|
5
|
+
### 1. Configure PyPI trusted publisher (OIDC)
|
|
6
|
+
|
|
7
|
+
On the PyPI side, before the first release:
|
|
8
|
+
|
|
9
|
+
1. Create the project page at <https://pypi.org/manage/account/publishing/>.
|
|
10
|
+
2. Add a **pending publisher** with:
|
|
11
|
+
* PyPI Project Name: `lamcp`
|
|
12
|
+
* Owner: `gramaziokohler`
|
|
13
|
+
* Repository name: `lamcp`
|
|
14
|
+
* Workflow name: `release.yml`
|
|
15
|
+
* Environment name: `pypi`
|
|
16
|
+
|
|
17
|
+
This lets the GitHub Actions workflow publish to PyPI without storing a
|
|
18
|
+
token in the repo (OIDC short-lived credentials instead).
|
|
19
|
+
|
|
20
|
+
### 2. Create the `pypi` environment in GitHub
|
|
21
|
+
|
|
22
|
+
In the repo settings → Environments → New environment → `pypi`.
|
|
23
|
+
|
|
24
|
+
Optionally add deployment protection rules (required reviewers, branch
|
|
25
|
+
restrictions). The `publish-pypi` job in `.github/workflows/release.yml`
|
|
26
|
+
runs in this environment.
|
|
27
|
+
|
|
28
|
+
## Cutting a release
|
|
29
|
+
|
|
30
|
+
1. Update `CHANGELOG.md`:
|
|
31
|
+
* Rename `## Unreleased` to `## [X.Y.Z] - YYYY-MM-DD`.
|
|
32
|
+
* Add a new empty `## Unreleased` section above it.
|
|
33
|
+
|
|
34
|
+
2. Bump the version in `pyproject.toml` (`project.version`).
|
|
35
|
+
|
|
36
|
+
3. Commit and push the bump:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git add pyproject.toml CHANGELOG.md
|
|
40
|
+
git commit -m "Bump version to X.Y.Z"
|
|
41
|
+
git push
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
4. Tag and push:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
git tag vX.Y.Z
|
|
48
|
+
git push origin vX.Y.Z
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. Watch the `release` workflow under Actions. It:
|
|
52
|
+
* Builds the Python sdist + wheel
|
|
53
|
+
* Builds `Lamcp_Bridge.ghuser` on Windows via
|
|
54
|
+
`compas-dev/compas-actions.ghpython_components`
|
|
55
|
+
* Publishes the wheel + sdist to PyPI via OIDC
|
|
56
|
+
* Creates a GitHub release with auto-generated notes, attaching the
|
|
57
|
+
wheel, sdist, and `Lamcp_Bridge.ghuser` as downloadable assets
|
|
58
|
+
|
|
59
|
+
## Versioning
|
|
60
|
+
|
|
61
|
+
SemVer. `0.x` is unstable — API can break in any minor. `1.0` is the
|
|
62
|
+
commitment to backward compatibility.
|