paraview-mcp-python 0.1.2__tar.gz → 0.1.4__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.
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/PKG-INFO +41 -48
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/README.md +40 -47
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/command_handler.py +3 -1
- paraview_mcp_python-0.1.4/bridge/gui_bridge.py +264 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/docs/architecture.md +22 -21
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/docs/python-execute-design.md +6 -4
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/pyproject.toml +2 -1
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/start_paraview_bridge.py +19 -1
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/start_paraview_gui_bridge.py +15 -6
- paraview_mcp_python-0.1.4/src/paraview_mcp_server/launcher.py +151 -0
- paraview_mcp_python-0.1.4/tests/test_gui_bridge.py +143 -0
- paraview_mcp_python-0.1.4/tests/test_launcher.py +63 -0
- paraview_mcp_python-0.1.2/bridge/gui_bridge.py +0 -55
- paraview_mcp_python-0.1.2/tests/test_gui_bridge.py +0 -55
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/.gitignore +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/LICENSE +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/__init__.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/execution.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/models.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/server.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/color_by.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/create_contour.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/create_slice.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/open_dataset.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/reset_camera.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/save_screenshot.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/paraview_bridge_request.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/src/paraview_mcp_server/__init__.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/src/paraview_mcp_server/headless.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/src/paraview_mcp_server/server.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/__init__.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_bridge_server.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_command_handler.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_protocol.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_server.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paraview-mcp-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: MCP server for controlling ParaView via AI assistants
|
|
5
5
|
Project-URL: Homepage, https://github.com/djeada/paraview-mcp-server
|
|
6
6
|
Project-URL: Repository, https://github.com/djeada/paraview-mcp-server
|
|
@@ -50,18 +50,21 @@ paraview-mcp-server
|
|
|
50
50
|
|
|
51
51
|
## What Parts Are There?
|
|
52
52
|
|
|
53
|
-
There are
|
|
53
|
+
There are four moving pieces in the GUI workflow:
|
|
54
54
|
|
|
55
55
|
| Part | Runs where | Purpose |
|
|
56
56
|
|---|---|---|
|
|
57
57
|
| **MCP client** | Codex CLI, Claude Desktop, or another MCP host | Starts the MCP server and calls tools. |
|
|
58
58
|
| **MCP server** | Normal Python environment | Speaks MCP over stdio and forwards tool calls to ParaView over TCP. |
|
|
59
|
-
| **ParaView
|
|
59
|
+
| **ParaView bridge** | `pvpython` process | Receives TCP JSON commands and executes `paraview.simple` operations. |
|
|
60
|
+
| **ParaView runtime** | `pvserver` plus a ParaView GUI client | Owns the shared ParaView session that the GUI and bridge both use. |
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
ParaView is not Blender: Python plugins and VTK timer callbacks are pipeline
|
|
63
|
+
extension mechanisms, not a safe general-purpose remote-control hook for a live
|
|
64
|
+
GUI process. The supported GUI workflow here uses ParaView's client/server
|
|
65
|
+
model. `paraview-mcp-launch` starts a local `pvserver`, connects the ParaView
|
|
66
|
+
GUI as the first client, then connects a `pvpython` bridge client to the same
|
|
67
|
+
session.
|
|
65
68
|
|
|
66
69
|
```
|
|
67
70
|
┌──────────────────────────────┐ stdio ┌────────────────────────┐
|
|
@@ -72,24 +75,24 @@ Python Shell using `scripts/start_paraview_gui_bridge.py`.
|
|
|
72
75
|
│ 127.0.0.1:9876
|
|
73
76
|
┌──────────▼─────────────┐
|
|
74
77
|
│ ParaView Bridge │
|
|
75
|
-
│
|
|
78
|
+
│ pvpython client │
|
|
76
79
|
└──────────┬─────────────┘
|
|
77
|
-
│
|
|
80
|
+
│ ParaView client/server
|
|
78
81
|
┌──────────▼─────────────┐
|
|
79
|
-
│
|
|
80
|
-
│ ParaView
|
|
82
|
+
│ pvserver + GUI client │
|
|
83
|
+
│ shared ParaView state │
|
|
81
84
|
└────────────────────────┘
|
|
82
85
|
```
|
|
83
86
|
|
|
84
87
|
Why a ParaView-side bridge? ParaView's useful automation API is
|
|
85
88
|
`paraview.simple`, and it must execute inside a ParaView Python runtime. The
|
|
86
|
-
MCP server itself is only a protocol adapter; it cannot modify a ParaView
|
|
87
|
-
unless
|
|
89
|
+
MCP server itself is only a protocol adapter; it cannot modify a ParaView
|
|
90
|
+
session unless a ParaView-side bridge is running.
|
|
88
91
|
|
|
89
92
|
For live GUI modification, use:
|
|
90
93
|
|
|
91
94
|
```text
|
|
92
|
-
Codex/Claude -> MCP server -> bridge
|
|
95
|
+
Codex/Claude -> MCP server -> pvpython bridge -> pvserver <- ParaView GUI
|
|
93
96
|
```
|
|
94
97
|
|
|
95
98
|
For headless automation, use:
|
|
@@ -118,6 +121,7 @@ This installs:
|
|
|
118
121
|
|
|
119
122
|
```bash
|
|
120
123
|
paraview-mcp-server
|
|
124
|
+
paraview-mcp-launch
|
|
121
125
|
```
|
|
122
126
|
|
|
123
127
|
### Option B: Clone this repository for the ParaView bridge
|
|
@@ -137,49 +141,37 @@ This creates:
|
|
|
137
141
|
|
|
138
142
|
```bash
|
|
139
143
|
.venv/bin/paraview-mcp-server
|
|
144
|
+
.venv/bin/paraview-mcp-launch
|
|
140
145
|
```
|
|
141
146
|
|
|
142
147
|
---
|
|
143
148
|
|
|
144
149
|
## Start Everything
|
|
145
150
|
|
|
146
|
-
Start the
|
|
151
|
+
Start the ParaView side with one command:
|
|
147
152
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
```bash
|
|
154
|
+
cd /path/to/paraview-mcp-server
|
|
155
|
+
paraview-mcp-launch
|
|
156
|
+
```
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
2. Click **Run Script**.
|
|
154
|
-
3. Select:
|
|
158
|
+
For a local editable checkout:
|
|
155
159
|
|
|
156
|
-
```
|
|
157
|
-
/
|
|
160
|
+
```bash
|
|
161
|
+
.venv/bin/paraview-mcp-launch
|
|
158
162
|
```
|
|
159
163
|
|
|
160
164
|
Expected output:
|
|
161
165
|
|
|
162
166
|
```text
|
|
163
|
-
ParaView MCP
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
If `paraview-mcp-python` is installed into ParaView's Python environment, you
|
|
167
|
-
can also start the live GUI bridge directly from the Python Shell:
|
|
168
|
-
|
|
169
|
-
```python
|
|
170
|
-
from bridge.gui_bridge import start_gui_bridge, stop_gui_bridge
|
|
171
|
-
start_gui_bridge()
|
|
167
|
+
ParaView MCP bridge ready on 127.0.0.1:9876
|
|
168
|
+
Launching ParaView GUI connected to cs://127.0.0.1:11111
|
|
172
169
|
```
|
|
173
170
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
To stop the bridge from the ParaView Python Shell:
|
|
177
|
-
|
|
178
|
-
```python
|
|
179
|
-
stop_gui_bridge()
|
|
180
|
-
```
|
|
171
|
+
Keep that terminal running. Closing it stops the GUI, bridge, and local
|
|
172
|
+
`pvserver` session.
|
|
181
173
|
|
|
182
|
-
###
|
|
174
|
+
### Optional: Start a Headless `pvpython` Bridge
|
|
183
175
|
|
|
184
176
|
Use this only when you do not need to modify an already-open ParaView GUI:
|
|
185
177
|
|
|
@@ -197,7 +189,7 @@ ParaView bridge ready on 127.0.0.1:9876
|
|
|
197
189
|
Keep that terminal running. This controls the `pvpython` session, not a GUI
|
|
198
190
|
window opened separately.
|
|
199
191
|
|
|
200
|
-
###
|
|
192
|
+
### Verify the Bridge Directly
|
|
201
193
|
|
|
202
194
|
Before involving an MCP client, send one raw bridge command:
|
|
203
195
|
|
|
@@ -219,7 +211,7 @@ Expected response shape:
|
|
|
219
211
|
|
|
220
212
|
If this fails, fix the bridge before configuring Codex or Claude.
|
|
221
213
|
|
|
222
|
-
###
|
|
214
|
+
### Register the MCP Server with Codex CLI
|
|
223
215
|
|
|
224
216
|
If you installed from PyPI:
|
|
225
217
|
|
|
@@ -238,7 +230,7 @@ codex mcp list
|
|
|
238
230
|
Codex starts the MCP server automatically when needed. The ParaView bridge
|
|
239
231
|
must already be running separately.
|
|
240
232
|
|
|
241
|
-
###
|
|
233
|
+
### Register with Claude Desktop
|
|
242
234
|
|
|
243
235
|
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
244
236
|
|
|
@@ -260,7 +252,7 @@ which paraview-mcp-server
|
|
|
260
252
|
|
|
261
253
|
Restart Claude Desktop after editing the config.
|
|
262
254
|
|
|
263
|
-
###
|
|
255
|
+
### Verify Through Your MCP Client
|
|
264
256
|
|
|
265
257
|
With the bridge still running, ask your MCP client:
|
|
266
258
|
|
|
@@ -269,8 +261,8 @@ List all sources in the current ParaView session.
|
|
|
269
261
|
```
|
|
270
262
|
|
|
271
263
|
The client should call `paraview_scene_list_sources` and return the current
|
|
272
|
-
ParaView pipeline sources from the
|
|
273
|
-
`
|
|
264
|
+
ParaView pipeline sources from the server-backed GUI session started by
|
|
265
|
+
`paraview-mcp-launch`.
|
|
274
266
|
|
|
275
267
|
---
|
|
276
268
|
|
|
@@ -480,16 +472,17 @@ paraview-mcp-server/
|
|
|
480
472
|
│ └── paraview_mcp_server/
|
|
481
473
|
│ ├── __init__.py # Re-exports main()
|
|
482
474
|
│ ├── server.py # FastMCP stdio server (31 tools)
|
|
475
|
+
│ ├── launcher.py # Starts pvserver, GUI, and bridge together
|
|
483
476
|
│ └── headless.py # Headless pvpython executor + job manager
|
|
484
477
|
├── bridge/
|
|
485
478
|
│ ├── __init__.py
|
|
486
479
|
│ ├── server.py # TCP socket bridge server
|
|
487
|
-
│ ├── gui_bridge.py #
|
|
480
|
+
│ ├── gui_bridge.py # Experimental in-GUI bridge helpers
|
|
488
481
|
│ ├── command_handler.py # Command registry + paraview.simple handlers (27 commands)
|
|
489
482
|
│ └── execution.py # trusted local python.execute helper
|
|
490
483
|
├── scripts/
|
|
491
|
-
│ ├── start_paraview_gui_bridge.py
|
|
492
484
|
│ ├── start_paraview_bridge.py
|
|
485
|
+
│ ├── start_paraview_gui_bridge.py
|
|
493
486
|
│ ├── paraview_bridge_request.py
|
|
494
487
|
│ └── library/ # Reusable pvpython snippets
|
|
495
488
|
│ ├── open_dataset.py
|
|
@@ -17,18 +17,21 @@ paraview-mcp-server
|
|
|
17
17
|
|
|
18
18
|
## What Parts Are There?
|
|
19
19
|
|
|
20
|
-
There are
|
|
20
|
+
There are four moving pieces in the GUI workflow:
|
|
21
21
|
|
|
22
22
|
| Part | Runs where | Purpose |
|
|
23
23
|
|---|---|---|
|
|
24
24
|
| **MCP client** | Codex CLI, Claude Desktop, or another MCP host | Starts the MCP server and calls tools. |
|
|
25
25
|
| **MCP server** | Normal Python environment | Speaks MCP over stdio and forwards tool calls to ParaView over TCP. |
|
|
26
|
-
| **ParaView
|
|
26
|
+
| **ParaView bridge** | `pvpython` process | Receives TCP JSON commands and executes `paraview.simple` operations. |
|
|
27
|
+
| **ParaView runtime** | `pvserver` plus a ParaView GUI client | Owns the shared ParaView session that the GUI and bridge both use. |
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
ParaView is not Blender: Python plugins and VTK timer callbacks are pipeline
|
|
30
|
+
extension mechanisms, not a safe general-purpose remote-control hook for a live
|
|
31
|
+
GUI process. The supported GUI workflow here uses ParaView's client/server
|
|
32
|
+
model. `paraview-mcp-launch` starts a local `pvserver`, connects the ParaView
|
|
33
|
+
GUI as the first client, then connects a `pvpython` bridge client to the same
|
|
34
|
+
session.
|
|
32
35
|
|
|
33
36
|
```
|
|
34
37
|
┌──────────────────────────────┐ stdio ┌────────────────────────┐
|
|
@@ -39,24 +42,24 @@ Python Shell using `scripts/start_paraview_gui_bridge.py`.
|
|
|
39
42
|
│ 127.0.0.1:9876
|
|
40
43
|
┌──────────▼─────────────┐
|
|
41
44
|
│ ParaView Bridge │
|
|
42
|
-
│
|
|
45
|
+
│ pvpython client │
|
|
43
46
|
└──────────┬─────────────┘
|
|
44
|
-
│
|
|
47
|
+
│ ParaView client/server
|
|
45
48
|
┌──────────▼─────────────┐
|
|
46
|
-
│
|
|
47
|
-
│ ParaView
|
|
49
|
+
│ pvserver + GUI client │
|
|
50
|
+
│ shared ParaView state │
|
|
48
51
|
└────────────────────────┘
|
|
49
52
|
```
|
|
50
53
|
|
|
51
54
|
Why a ParaView-side bridge? ParaView's useful automation API is
|
|
52
55
|
`paraview.simple`, and it must execute inside a ParaView Python runtime. The
|
|
53
|
-
MCP server itself is only a protocol adapter; it cannot modify a ParaView
|
|
54
|
-
unless
|
|
56
|
+
MCP server itself is only a protocol adapter; it cannot modify a ParaView
|
|
57
|
+
session unless a ParaView-side bridge is running.
|
|
55
58
|
|
|
56
59
|
For live GUI modification, use:
|
|
57
60
|
|
|
58
61
|
```text
|
|
59
|
-
Codex/Claude -> MCP server -> bridge
|
|
62
|
+
Codex/Claude -> MCP server -> pvpython bridge -> pvserver <- ParaView GUI
|
|
60
63
|
```
|
|
61
64
|
|
|
62
65
|
For headless automation, use:
|
|
@@ -85,6 +88,7 @@ This installs:
|
|
|
85
88
|
|
|
86
89
|
```bash
|
|
87
90
|
paraview-mcp-server
|
|
91
|
+
paraview-mcp-launch
|
|
88
92
|
```
|
|
89
93
|
|
|
90
94
|
### Option B: Clone this repository for the ParaView bridge
|
|
@@ -104,49 +108,37 @@ This creates:
|
|
|
104
108
|
|
|
105
109
|
```bash
|
|
106
110
|
.venv/bin/paraview-mcp-server
|
|
111
|
+
.venv/bin/paraview-mcp-launch
|
|
107
112
|
```
|
|
108
113
|
|
|
109
114
|
---
|
|
110
115
|
|
|
111
116
|
## Start Everything
|
|
112
117
|
|
|
113
|
-
Start the
|
|
118
|
+
Start the ParaView side with one command:
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
```bash
|
|
121
|
+
cd /path/to/paraview-mcp-server
|
|
122
|
+
paraview-mcp-launch
|
|
123
|
+
```
|
|
118
124
|
|
|
119
|
-
|
|
120
|
-
2. Click **Run Script**.
|
|
121
|
-
3. Select:
|
|
125
|
+
For a local editable checkout:
|
|
122
126
|
|
|
123
|
-
```
|
|
124
|
-
/
|
|
127
|
+
```bash
|
|
128
|
+
.venv/bin/paraview-mcp-launch
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
Expected output:
|
|
128
132
|
|
|
129
133
|
```text
|
|
130
|
-
ParaView MCP
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
If `paraview-mcp-python` is installed into ParaView's Python environment, you
|
|
134
|
-
can also start the live GUI bridge directly from the Python Shell:
|
|
135
|
-
|
|
136
|
-
```python
|
|
137
|
-
from bridge.gui_bridge import start_gui_bridge, stop_gui_bridge
|
|
138
|
-
start_gui_bridge()
|
|
134
|
+
ParaView MCP bridge ready on 127.0.0.1:9876
|
|
135
|
+
Launching ParaView GUI connected to cs://127.0.0.1:11111
|
|
139
136
|
```
|
|
140
137
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
To stop the bridge from the ParaView Python Shell:
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
stop_gui_bridge()
|
|
147
|
-
```
|
|
138
|
+
Keep that terminal running. Closing it stops the GUI, bridge, and local
|
|
139
|
+
`pvserver` session.
|
|
148
140
|
|
|
149
|
-
###
|
|
141
|
+
### Optional: Start a Headless `pvpython` Bridge
|
|
150
142
|
|
|
151
143
|
Use this only when you do not need to modify an already-open ParaView GUI:
|
|
152
144
|
|
|
@@ -164,7 +156,7 @@ ParaView bridge ready on 127.0.0.1:9876
|
|
|
164
156
|
Keep that terminal running. This controls the `pvpython` session, not a GUI
|
|
165
157
|
window opened separately.
|
|
166
158
|
|
|
167
|
-
###
|
|
159
|
+
### Verify the Bridge Directly
|
|
168
160
|
|
|
169
161
|
Before involving an MCP client, send one raw bridge command:
|
|
170
162
|
|
|
@@ -186,7 +178,7 @@ Expected response shape:
|
|
|
186
178
|
|
|
187
179
|
If this fails, fix the bridge before configuring Codex or Claude.
|
|
188
180
|
|
|
189
|
-
###
|
|
181
|
+
### Register the MCP Server with Codex CLI
|
|
190
182
|
|
|
191
183
|
If you installed from PyPI:
|
|
192
184
|
|
|
@@ -205,7 +197,7 @@ codex mcp list
|
|
|
205
197
|
Codex starts the MCP server automatically when needed. The ParaView bridge
|
|
206
198
|
must already be running separately.
|
|
207
199
|
|
|
208
|
-
###
|
|
200
|
+
### Register with Claude Desktop
|
|
209
201
|
|
|
210
202
|
**Claude Desktop** — add to `claude_desktop_config.json`:
|
|
211
203
|
|
|
@@ -227,7 +219,7 @@ which paraview-mcp-server
|
|
|
227
219
|
|
|
228
220
|
Restart Claude Desktop after editing the config.
|
|
229
221
|
|
|
230
|
-
###
|
|
222
|
+
### Verify Through Your MCP Client
|
|
231
223
|
|
|
232
224
|
With the bridge still running, ask your MCP client:
|
|
233
225
|
|
|
@@ -236,8 +228,8 @@ List all sources in the current ParaView session.
|
|
|
236
228
|
```
|
|
237
229
|
|
|
238
230
|
The client should call `paraview_scene_list_sources` and return the current
|
|
239
|
-
ParaView pipeline sources from the
|
|
240
|
-
`
|
|
231
|
+
ParaView pipeline sources from the server-backed GUI session started by
|
|
232
|
+
`paraview-mcp-launch`.
|
|
241
233
|
|
|
242
234
|
---
|
|
243
235
|
|
|
@@ -447,16 +439,17 @@ paraview-mcp-server/
|
|
|
447
439
|
│ └── paraview_mcp_server/
|
|
448
440
|
│ ├── __init__.py # Re-exports main()
|
|
449
441
|
│ ├── server.py # FastMCP stdio server (31 tools)
|
|
442
|
+
│ ├── launcher.py # Starts pvserver, GUI, and bridge together
|
|
450
443
|
│ └── headless.py # Headless pvpython executor + job manager
|
|
451
444
|
├── bridge/
|
|
452
445
|
│ ├── __init__.py
|
|
453
446
|
│ ├── server.py # TCP socket bridge server
|
|
454
|
-
│ ├── gui_bridge.py #
|
|
447
|
+
│ ├── gui_bridge.py # Experimental in-GUI bridge helpers
|
|
455
448
|
│ ├── command_handler.py # Command registry + paraview.simple handlers (27 commands)
|
|
456
449
|
│ └── execution.py # trusted local python.execute helper
|
|
457
450
|
├── scripts/
|
|
458
|
-
│ ├── start_paraview_gui_bridge.py
|
|
459
451
|
│ ├── start_paraview_bridge.py
|
|
452
|
+
│ ├── start_paraview_gui_bridge.py
|
|
460
453
|
│ ├── paraview_bridge_request.py
|
|
461
454
|
│ └── library/ # Reusable pvpython snippets
|
|
462
455
|
│ ├── open_dataset.py
|
|
@@ -8,6 +8,7 @@ is available.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
+
import os
|
|
11
12
|
from typing import TYPE_CHECKING, Any
|
|
12
13
|
|
|
13
14
|
from bridge.models import (
|
|
@@ -346,7 +347,8 @@ class CommandHandler:
|
|
|
346
347
|
camera.SetViewUp(*params["view_up"])
|
|
347
348
|
if "parallel_scale" in params:
|
|
348
349
|
camera.SetParallelScale(params["parallel_scale"])
|
|
349
|
-
|
|
350
|
+
if os.environ.get("PARAVIEW_MCP_GUI_BRIDGE") != "1":
|
|
351
|
+
view.StillRender()
|
|
350
352
|
pos = list(camera.GetPosition())
|
|
351
353
|
fp = list(camera.GetFocalPoint())
|
|
352
354
|
up = list(camera.GetViewUp())
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Helpers for starting the bridge from an already-open ParaView GUI session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import select
|
|
10
|
+
import socket
|
|
11
|
+
import traceback
|
|
12
|
+
import uuid
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from bridge.server import BUFFER_SIZE, HOST, PORT
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class _ClientState:
|
|
23
|
+
sock: socket.socket
|
|
24
|
+
buffer: bytes = b""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ParaViewGuiBridgeServer:
|
|
28
|
+
"""Nonblocking TCP bridge polled from ParaView's GUI event loop."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, host: str = HOST, port: int = PORT, poll_interval_ms: int = 50):
|
|
31
|
+
self._host = host
|
|
32
|
+
self._port = port
|
|
33
|
+
self._poll_interval_ms = poll_interval_ms
|
|
34
|
+
self._server_socket: socket.socket | None = None
|
|
35
|
+
self._clients: dict[socket.socket, _ClientState] = {}
|
|
36
|
+
self._running = False
|
|
37
|
+
self._interactor: Any | None = None
|
|
38
|
+
self._observer_id: int | None = None
|
|
39
|
+
self._timer_id: int | None = None
|
|
40
|
+
# Import here so this module can be imported without ParaView installed.
|
|
41
|
+
from bridge.command_handler import CommandHandler
|
|
42
|
+
|
|
43
|
+
self._handler = CommandHandler()
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def host(self) -> str:
|
|
47
|
+
return self._host
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def port(self) -> int:
|
|
51
|
+
return self._port
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_running(self) -> bool:
|
|
55
|
+
return self._running
|
|
56
|
+
|
|
57
|
+
def start(self) -> None:
|
|
58
|
+
if self._running:
|
|
59
|
+
return
|
|
60
|
+
self._interactor = self._get_render_window_interactor()
|
|
61
|
+
if not callable(getattr(self._interactor, "AddObserver", None)):
|
|
62
|
+
raise RuntimeError("ParaView render window interactor does not support VTK observers")
|
|
63
|
+
if not callable(getattr(self._interactor, "CreateRepeatingTimer", None)):
|
|
64
|
+
raise RuntimeError("ParaView render window interactor does not support repeating timers")
|
|
65
|
+
|
|
66
|
+
self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
67
|
+
self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
68
|
+
self._server_socket.setblocking(False)
|
|
69
|
+
self._server_socket.bind((self._host, self._port))
|
|
70
|
+
self._host, self._port = self._server_socket.getsockname()[:2]
|
|
71
|
+
self._server_socket.listen(5)
|
|
72
|
+
|
|
73
|
+
self._observer_id = self._interactor.AddObserver("TimerEvent", self._on_timer)
|
|
74
|
+
self._timer_id = self._interactor.CreateRepeatingTimer(self._poll_interval_ms)
|
|
75
|
+
os.environ["PARAVIEW_MCP_GUI_BRIDGE"] = "1"
|
|
76
|
+
self._running = True
|
|
77
|
+
logger.info("ParaView GUI bridge listening on %s:%s", self._host, self._port)
|
|
78
|
+
|
|
79
|
+
def stop(self) -> None:
|
|
80
|
+
self._running = False
|
|
81
|
+
if self._interactor is not None and self._timer_id is not None:
|
|
82
|
+
destroy_timer = getattr(self._interactor, "DestroyTimer", None)
|
|
83
|
+
if callable(destroy_timer):
|
|
84
|
+
with contextlib.suppress(Exception):
|
|
85
|
+
destroy_timer(self._timer_id)
|
|
86
|
+
if self._interactor is not None and self._observer_id is not None:
|
|
87
|
+
remove_observer = getattr(self._interactor, "RemoveObserver", None)
|
|
88
|
+
if callable(remove_observer):
|
|
89
|
+
with contextlib.suppress(Exception):
|
|
90
|
+
remove_observer(self._observer_id)
|
|
91
|
+
self._timer_id = None
|
|
92
|
+
self._observer_id = None
|
|
93
|
+
self._interactor = None
|
|
94
|
+
os.environ.pop("PARAVIEW_MCP_GUI_BRIDGE", None)
|
|
95
|
+
|
|
96
|
+
for client in list(self._clients):
|
|
97
|
+
self._close_client(client)
|
|
98
|
+
if self._server_socket is not None:
|
|
99
|
+
with contextlib.suppress(OSError):
|
|
100
|
+
self._server_socket.close()
|
|
101
|
+
self._server_socket = None
|
|
102
|
+
|
|
103
|
+
def _on_timer(self, _obj: Any, _event: str) -> None:
|
|
104
|
+
self.poll()
|
|
105
|
+
|
|
106
|
+
def poll(self) -> None:
|
|
107
|
+
"""Process pending socket work without blocking the GUI event loop."""
|
|
108
|
+
if not self._running or self._server_socket is None:
|
|
109
|
+
return
|
|
110
|
+
sockets = [self._server_socket, *self._clients]
|
|
111
|
+
try:
|
|
112
|
+
readable, _, errored = select.select(sockets, [], sockets, 0)
|
|
113
|
+
except OSError:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
for sock in errored:
|
|
117
|
+
if sock is self._server_socket:
|
|
118
|
+
logger.error("ParaView GUI bridge server socket failed")
|
|
119
|
+
self.stop()
|
|
120
|
+
return
|
|
121
|
+
self._close_client(sock)
|
|
122
|
+
|
|
123
|
+
for sock in readable:
|
|
124
|
+
if sock is self._server_socket:
|
|
125
|
+
self._accept_ready_clients()
|
|
126
|
+
return
|
|
127
|
+
else:
|
|
128
|
+
if self._read_client(sock):
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
def _accept_ready_clients(self) -> None:
|
|
132
|
+
if self._server_socket is None:
|
|
133
|
+
return
|
|
134
|
+
while True:
|
|
135
|
+
try:
|
|
136
|
+
conn, addr = self._server_socket.accept()
|
|
137
|
+
except BlockingIOError:
|
|
138
|
+
break
|
|
139
|
+
except OSError:
|
|
140
|
+
break
|
|
141
|
+
conn.setblocking(False)
|
|
142
|
+
self._clients[conn] = _ClientState(conn)
|
|
143
|
+
logger.info("Client connected from %s", addr)
|
|
144
|
+
|
|
145
|
+
def _read_client(self, sock: socket.socket) -> bool:
|
|
146
|
+
state = self._clients.get(sock)
|
|
147
|
+
if state is None:
|
|
148
|
+
return False
|
|
149
|
+
try:
|
|
150
|
+
data = sock.recv(BUFFER_SIZE)
|
|
151
|
+
except BlockingIOError:
|
|
152
|
+
return False
|
|
153
|
+
except OSError:
|
|
154
|
+
self._close_client(sock)
|
|
155
|
+
return False
|
|
156
|
+
if not data:
|
|
157
|
+
self._close_client(sock)
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
state.buffer += data
|
|
161
|
+
while b"\n" in state.buffer:
|
|
162
|
+
line, state.buffer = state.buffer.split(b"\n", 1)
|
|
163
|
+
if not line.strip():
|
|
164
|
+
continue
|
|
165
|
+
try:
|
|
166
|
+
request = json.loads(line.decode("utf-8"))
|
|
167
|
+
response = self._process_request(request)
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
response = {"id": None, "success": False, "error": str(exc)}
|
|
170
|
+
self._send_response(sock, response)
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def _send_response(self, sock: socket.socket, response: dict[str, Any]) -> None:
|
|
175
|
+
try:
|
|
176
|
+
sock.sendall((json.dumps(response) + "\n").encode("utf-8"))
|
|
177
|
+
except OSError:
|
|
178
|
+
self._close_client(sock)
|
|
179
|
+
|
|
180
|
+
def _close_client(self, sock: socket.socket) -> None:
|
|
181
|
+
self._clients.pop(sock, None)
|
|
182
|
+
with contextlib.suppress(OSError):
|
|
183
|
+
sock.close()
|
|
184
|
+
|
|
185
|
+
def _process_request(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
186
|
+
if not isinstance(request, dict):
|
|
187
|
+
raise TypeError("Request must be a JSON object")
|
|
188
|
+
|
|
189
|
+
req_id = request.get("id", str(uuid.uuid4()))
|
|
190
|
+
command = request.get("command")
|
|
191
|
+
params = request.get("params", {})
|
|
192
|
+
if not isinstance(command, str) or not command.strip():
|
|
193
|
+
return {"id": req_id, "success": False, "error": "Missing or invalid command"}
|
|
194
|
+
if not isinstance(params, dict):
|
|
195
|
+
return {"id": req_id, "success": False, "error": "Invalid params: expected JSON object"}
|
|
196
|
+
try:
|
|
197
|
+
result = self._handler.handle(command, params)
|
|
198
|
+
return {"id": req_id, "success": True, "result": result}
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
logger.error("Command '%s' failed: %s\n%s", command, exc, traceback.format_exc())
|
|
201
|
+
return {"id": req_id, "success": False, "error": str(exc)}
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _get_render_window_interactor() -> Any:
|
|
205
|
+
import paraview # noqa: PLC0415
|
|
206
|
+
import paraview.simple as pvs # noqa: PLC0415
|
|
207
|
+
|
|
208
|
+
if not getattr(paraview, "fromGUI", False):
|
|
209
|
+
raise RuntimeError(
|
|
210
|
+
"The live GUI bridge must be started from ParaView's Python Shell with Run Script. "
|
|
211
|
+
"Starting it with 'paraview --script' runs too early in ParaView startup and is not stable."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
view = pvs.GetActiveViewOrCreate("RenderView")
|
|
215
|
+
render_window = view.GetRenderWindow()
|
|
216
|
+
interactor = render_window.GetInteractor()
|
|
217
|
+
if interactor is None:
|
|
218
|
+
raise RuntimeError("ParaView render window interactor is not available")
|
|
219
|
+
return interactor
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
_SERVER: ParaViewGuiBridgeServer | None = None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def start_gui_bridge(host: str = HOST, port: int = PORT) -> dict[str, Any]:
|
|
226
|
+
"""Start the bridge inside the current ParaView GUI process."""
|
|
227
|
+
global _SERVER
|
|
228
|
+
if _SERVER is not None and _SERVER.is_running:
|
|
229
|
+
return {
|
|
230
|
+
"host": _SERVER.host,
|
|
231
|
+
"port": _SERVER.port,
|
|
232
|
+
"running": True,
|
|
233
|
+
"already_running": True,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
server = ParaViewGuiBridgeServer(host=host, port=port)
|
|
237
|
+
server.start()
|
|
238
|
+
_SERVER = server
|
|
239
|
+
return {
|
|
240
|
+
"host": server.host,
|
|
241
|
+
"port": server.port,
|
|
242
|
+
"running": True,
|
|
243
|
+
"already_running": False,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def stop_gui_bridge() -> dict[str, Any]:
|
|
248
|
+
"""Stop the bridge started by :func:`start_gui_bridge`."""
|
|
249
|
+
global _SERVER
|
|
250
|
+
if _SERVER is None:
|
|
251
|
+
return {"running": False, "stopped": False}
|
|
252
|
+
|
|
253
|
+
host = _SERVER.host
|
|
254
|
+
port = _SERVER.port
|
|
255
|
+
_SERVER.stop()
|
|
256
|
+
_SERVER = None
|
|
257
|
+
return {"host": host, "port": port, "running": False, "stopped": True}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def gui_bridge_status() -> dict[str, Any]:
|
|
261
|
+
"""Return the current GUI bridge status."""
|
|
262
|
+
if _SERVER is None or not _SERVER.is_running:
|
|
263
|
+
return {"running": False}
|
|
264
|
+
return {"host": _SERVER.host, "port": _SERVER.port, "running": True}
|