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.
@@ -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__/
@@ -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
@@ -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.