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.
Files changed (35) hide show
  1. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/PKG-INFO +41 -48
  2. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/README.md +40 -47
  3. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/command_handler.py +3 -1
  4. paraview_mcp_python-0.1.4/bridge/gui_bridge.py +264 -0
  5. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/docs/architecture.md +22 -21
  6. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/docs/python-execute-design.md +6 -4
  7. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/pyproject.toml +2 -1
  8. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/start_paraview_bridge.py +19 -1
  9. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/start_paraview_gui_bridge.py +15 -6
  10. paraview_mcp_python-0.1.4/src/paraview_mcp_server/launcher.py +151 -0
  11. paraview_mcp_python-0.1.4/tests/test_gui_bridge.py +143 -0
  12. paraview_mcp_python-0.1.4/tests/test_launcher.py +63 -0
  13. paraview_mcp_python-0.1.2/bridge/gui_bridge.py +0 -55
  14. paraview_mcp_python-0.1.2/tests/test_gui_bridge.py +0 -55
  15. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/.gitignore +0 -0
  16. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/LICENSE +0 -0
  17. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/__init__.py +0 -0
  18. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/execution.py +0 -0
  19. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/models.py +0 -0
  20. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/bridge/server.py +0 -0
  21. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/color_by.py +0 -0
  22. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/create_contour.py +0 -0
  23. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/create_slice.py +0 -0
  24. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/open_dataset.py +0 -0
  25. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/reset_camera.py +0 -0
  26. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/library/save_screenshot.py +0 -0
  27. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/scripts/paraview_bridge_request.py +0 -0
  28. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/src/paraview_mcp_server/__init__.py +0 -0
  29. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/src/paraview_mcp_server/headless.py +0 -0
  30. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/src/paraview_mcp_server/server.py +0 -0
  31. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/__init__.py +0 -0
  32. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_bridge_server.py +0 -0
  33. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_command_handler.py +0 -0
  34. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.4}/tests/test_protocol.py +0 -0
  35. {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.2
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 three moving pieces:
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 GUI bridge** | Already-open ParaView GUI process | Receives TCP JSON commands and executes `paraview.simple` operations in that live GUI session. |
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
- The ParaView bridge is the ParaView-side component. It plays the same role as
62
- the Blender add-on in `blender-mcp-server`: it must run inside the application
63
- you want to control. For live GUI control, start the bridge from ParaView's
64
- Python Shell using `scripts/start_paraview_gui_bridge.py`.
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
- live GUI process
78
+ pvpython client
76
79
  └──────────┬─────────────┘
77
-
80
+ ParaView client/server
78
81
  ┌──────────▼─────────────┐
79
- paraview.simple
80
- │ ParaView runtime
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 GUI
87
- unless the bridge is running inside that GUI process.
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 inside open ParaView GUI -> live GUI session
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 pieces in this order.
151
+ Start the ParaView side with one command:
147
152
 
148
- ### 1. Start the ParaView GUI Bridge
149
-
150
- Open ParaView, then run the GUI bridge script from the Python Shell:
153
+ ```bash
154
+ cd /path/to/paraview-mcp-server
155
+ paraview-mcp-launch
156
+ ```
151
157
 
152
- 1. In ParaView, open **Tools -> Python Shell**.
153
- 2. Click **Run Script**.
154
- 3. Select:
158
+ For a local editable checkout:
155
159
 
156
- ```text
157
- /absolute/path/to/paraview-mcp-server/scripts/start_paraview_gui_bridge.py
160
+ ```bash
161
+ .venv/bin/paraview-mcp-launch
158
162
  ```
159
163
 
160
164
  Expected output:
161
165
 
162
166
  ```text
163
- ParaView MCP GUI bridge started on 127.0.0.1:9876
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
- Leave ParaView open. MCP commands now modify this live GUI session.
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
- ### 2. Optional: Start a Headless `pvpython` Bridge
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
- ### 3. Verify the Bridge Directly
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
- ### 4. Register the MCP Server with Codex CLI
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
- ### 5. Register with Claude Desktop
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
- ### 6. Verify Through Your MCP Client
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 live GUI session if you started
273
- `start_paraview_gui_bridge.py`.
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 # Non-blocking live GUI bridge lifecycle
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 three moving pieces:
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 GUI bridge** | Already-open ParaView GUI process | Receives TCP JSON commands and executes `paraview.simple` operations in that live GUI session. |
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
- The ParaView bridge is the ParaView-side component. It plays the same role as
29
- the Blender add-on in `blender-mcp-server`: it must run inside the application
30
- you want to control. For live GUI control, start the bridge from ParaView's
31
- Python Shell using `scripts/start_paraview_gui_bridge.py`.
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
- live GUI process
45
+ pvpython client
43
46
  └──────────┬─────────────┘
44
-
47
+ ParaView client/server
45
48
  ┌──────────▼─────────────┐
46
- paraview.simple
47
- │ ParaView runtime
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 GUI
54
- unless the bridge is running inside that GUI process.
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 inside open ParaView GUI -> live GUI session
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 pieces in this order.
118
+ Start the ParaView side with one command:
114
119
 
115
- ### 1. Start the ParaView GUI Bridge
116
-
117
- Open ParaView, then run the GUI bridge script from the Python Shell:
120
+ ```bash
121
+ cd /path/to/paraview-mcp-server
122
+ paraview-mcp-launch
123
+ ```
118
124
 
119
- 1. In ParaView, open **Tools -> Python Shell**.
120
- 2. Click **Run Script**.
121
- 3. Select:
125
+ For a local editable checkout:
122
126
 
123
- ```text
124
- /absolute/path/to/paraview-mcp-server/scripts/start_paraview_gui_bridge.py
127
+ ```bash
128
+ .venv/bin/paraview-mcp-launch
125
129
  ```
126
130
 
127
131
  Expected output:
128
132
 
129
133
  ```text
130
- ParaView MCP GUI bridge started on 127.0.0.1:9876
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
- Leave ParaView open. MCP commands now modify this live GUI session.
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
- ### 2. Optional: Start a Headless `pvpython` Bridge
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
- ### 3. Verify the Bridge Directly
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
- ### 4. Register the MCP Server with Codex CLI
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
- ### 5. Register with Claude Desktop
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
- ### 6. Verify Through Your MCP Client
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 live GUI session if you started
240
- `start_paraview_gui_bridge.py`.
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 # Non-blocking live GUI bridge lifecycle
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
- view.StillRender()
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}