paraview-mcp-python 0.1.0__tar.gz → 0.1.2__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 (31) hide show
  1. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/PKG-INFO +211 -36
  2. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/README.md +210 -35
  3. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/bridge/execution.py +2 -78
  4. paraview_mcp_python-0.1.2/bridge/gui_bridge.py +55 -0
  5. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/bridge/models.py +2 -2
  6. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/bridge/server.py +13 -0
  7. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/docs/architecture.md +29 -9
  8. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/docs/python-execute-design.md +11 -7
  9. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/pyproject.toml +2 -2
  10. paraview_mcp_python-0.1.2/scripts/start_paraview_gui_bridge.py +42 -0
  11. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/tests/test_command_handler.py +7 -6
  12. paraview_mcp_python-0.1.2/tests/test_gui_bridge.py +55 -0
  13. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/.gitignore +0 -0
  14. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/LICENSE +0 -0
  15. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/bridge/__init__.py +0 -0
  16. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/bridge/command_handler.py +0 -0
  17. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/library/color_by.py +0 -0
  18. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/library/create_contour.py +0 -0
  19. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/library/create_slice.py +0 -0
  20. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/library/open_dataset.py +0 -0
  21. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/library/reset_camera.py +0 -0
  22. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/library/save_screenshot.py +0 -0
  23. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/paraview_bridge_request.py +0 -0
  24. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/scripts/start_paraview_bridge.py +0 -0
  25. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/src/paraview_mcp_server/__init__.py +0 -0
  26. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/src/paraview_mcp_server/headless.py +0 -0
  27. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/src/paraview_mcp_server/server.py +0 -0
  28. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/tests/__init__.py +0 -0
  29. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/tests/test_bridge_server.py +0 -0
  30. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/tests/test_protocol.py +0 -0
  31. {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.2}/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.0
3
+ Version: 0.1.2
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
@@ -31,50 +31,99 @@ Requires-Dist: pytest>=7.0; extra == 'dev'
31
31
  Requires-Dist: ruff>=0.11; extra == 'dev'
32
32
  Description-Content-Type: text/markdown
33
33
 
34
- # paraview-mcp-server
34
+ # ParaView MCP Server
35
35
 
36
36
  **Control ParaView with AI assistants through the Model Context Protocol.**
37
37
 
38
- `paraview-mcp-server` is a two-process bridge that lets AI assistants such as Claude Desktop
39
- and Codex CLI open datasets, apply filters, color data, and export screenshots in ParaView
40
- using natural language.
38
+ `paraview-mcp-python` provides an MCP server plus a ParaView-side bridge so
39
+ AI assistants such as Codex CLI and Claude Desktop can inspect a ParaView
40
+ session, open datasets, apply filters, color data, run ParaView Python, and
41
+ export screenshots.
42
+
43
+ The command installed for MCP clients is still:
44
+
45
+ ```bash
46
+ paraview-mcp-server
47
+ ```
41
48
 
42
49
  ---
43
50
 
44
- ## How it works
51
+ ## What Parts Are There?
52
+
53
+ There are three moving pieces:
54
+
55
+ | Part | Runs where | Purpose |
56
+ |---|---|---|
57
+ | **MCP client** | Codex CLI, Claude Desktop, or another MCP host | Starts the MCP server and calls tools. |
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. |
60
+
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`.
45
65
 
46
66
  ```
47
- MCP Client (Claude Desktop, Codex CLI, …)
48
- ⇅ stdio
49
- paraview-mcp-server ← thin MCP server, defines 31 tools
50
- ⇅ JSON / TCP localhost:9876
51
- ParaView bridge (pvpython) ← dispatches commands with paraview.simple
52
-
53
- paraview.simple / servermanager
67
+ ┌──────────────────────────────┐ stdio ┌────────────────────────┐
68
+ │ MCP Client │ ◄──────────────► │ MCP Server │
69
+ Codex / Claude / other host │ │ paraview-mcp-server │
70
+ └──────────────────────────────┘ └──────────┬─────────────┘
71
+ JSON/TCP
72
+ │ 127.0.0.1:9876
73
+ ┌──────────▼─────────────┐
74
+ │ ParaView Bridge │
75
+ │ live GUI process │
76
+ └──────────┬─────────────┘
77
+
78
+ ┌──────────▼─────────────┐
79
+ │ paraview.simple │
80
+ │ ParaView runtime │
81
+ └────────────────────────┘
54
82
  ```
55
83
 
56
- - The **MCP server** is a normal Python package. It speaks MCP over stdio and forwards
57
- every tool call as a JSON request to the bridge over a local TCP socket.
58
- - The **bridge** runs inside `pvpython`. It receives JSON commands, dispatches them through
59
- a command registry, calls `paraview.simple`, and returns JSON results.
60
- - Neither process depends on the other's code at import time.
61
- - A **headless pvpython executor** lets the MCP server run scripts in a separate
62
- `pvpython` process for long-running or async workflows (no bridge needed).
84
+ Why a ParaView-side bridge? ParaView's useful automation API is
85
+ `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.
88
+
89
+ For live GUI modification, use:
90
+
91
+ ```text
92
+ Codex/Claude -> MCP server -> bridge inside open ParaView GUI -> live GUI session
93
+ ```
94
+
95
+ For headless automation, use:
96
+
97
+ ```text
98
+ Codex/Claude -> MCP server -> bridge inside pvpython -> headless ParaView runtime
99
+ ```
63
100
 
64
101
  See [`docs/architecture.md`](docs/architecture.md) for a full diagram, protocol reference,
65
102
  and tool namespace table.
66
103
 
67
104
  ---
68
105
 
69
- ## Quick start
106
+ ## Install
70
107
 
71
- ### 1. Install the MCP server
108
+ ### Option A: Install the MCP server from PyPI
109
+
110
+ Use this when you only need the MCP server executable in your normal Python
111
+ environment:
72
112
 
73
113
  ```bash
74
114
  pip install paraview-mcp-python
75
115
  ```
76
116
 
77
- For local development from this repository:
117
+ This installs:
118
+
119
+ ```bash
120
+ paraview-mcp-server
121
+ ```
122
+
123
+ ### Option B: Clone this repository for the ParaView bridge
124
+
125
+ The bridge code must be available to ParaView's Python runtime. For live GUI
126
+ control and local development, clone the repository:
78
127
 
79
128
  ```bash
80
129
  git clone https://github.com/djeada/paraview-mcp-server.git
@@ -84,18 +133,112 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
84
133
  pip install -e .
85
134
  ```
86
135
 
87
- ### 2. Start the ParaView bridge
136
+ This creates:
137
+
138
+ ```bash
139
+ .venv/bin/paraview-mcp-server
140
+ ```
141
+
142
+ ---
143
+
144
+ ## Start Everything
145
+
146
+ Start the pieces in this order.
147
+
148
+ ### 1. Start the ParaView GUI Bridge
149
+
150
+ Open ParaView, then run the GUI bridge script from the Python Shell:
151
+
152
+ 1. In ParaView, open **Tools -> Python Shell**.
153
+ 2. Click **Run Script**.
154
+ 3. Select:
155
+
156
+ ```text
157
+ /absolute/path/to/paraview-mcp-server/scripts/start_paraview_gui_bridge.py
158
+ ```
159
+
160
+ Expected output:
161
+
162
+ ```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()
172
+ ```
173
+
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
+ ```
181
+
182
+ ### 2. Optional: Start a Headless `pvpython` Bridge
88
183
 
89
- In a terminal that has `pvpython` on `PATH`:
184
+ Use this only when you do not need to modify an already-open ParaView GUI:
90
185
 
91
186
  ```bash
187
+ cd /path/to/paraview-mcp-server
92
188
  pvpython scripts/start_paraview_bridge.py
93
- # → ParaView bridge ready on 127.0.0.1:9876
94
189
  ```
95
190
 
96
- The bridge listens for JSON commands from the MCP server.
191
+ Expected output:
192
+
193
+ ```text
194
+ ParaView bridge ready on 127.0.0.1:9876
195
+ ```
196
+
197
+ Keep that terminal running. This controls the `pvpython` session, not a GUI
198
+ window opened separately.
199
+
200
+ ### 3. Verify the Bridge Directly
201
+
202
+ Before involving an MCP client, send one raw bridge command:
203
+
204
+ ```bash
205
+ python scripts/paraview_bridge_request.py scene.get_info
206
+ ```
207
+
208
+ Expected response shape:
209
+
210
+ ```json
211
+ {
212
+ "success": true,
213
+ "result": {
214
+ "source_count": 0,
215
+ "active_view_type": "RenderView"
216
+ }
217
+ }
218
+ ```
219
+
220
+ If this fails, fix the bridge before configuring Codex or Claude.
221
+
222
+ ### 4. Register the MCP Server with Codex CLI
223
+
224
+ If you installed from PyPI:
225
+
226
+ ```bash
227
+ codex mcp add paraview -- paraview-mcp-server
228
+ codex mcp list
229
+ ```
230
+
231
+ If you are using the local repository:
97
232
 
98
- ### 3. Register the MCP server with your AI client
233
+ ```bash
234
+ codex mcp add paraview -- /absolute/path/to/paraview-mcp-server/.venv/bin/paraview-mcp-server
235
+ codex mcp list
236
+ ```
237
+
238
+ Codex starts the MCP server automatically when needed. The ParaView bridge
239
+ must already be running separately.
240
+
241
+ ### 5. Register with Claude Desktop
99
242
 
100
243
  **Claude Desktop** — add to `claude_desktop_config.json`:
101
244
 
@@ -109,12 +252,42 @@ The bridge listens for JSON commands from the MCP server.
109
252
  }
110
253
  ```
111
254
 
112
- **Codex CLI:**
255
+ For a PyPI install, use the absolute path returned by:
113
256
 
114
257
  ```bash
115
- codex mcp add paraview -- /absolute/path/to/.venv/bin/paraview-mcp-server
258
+ which paraview-mcp-server
116
259
  ```
117
260
 
261
+ Restart Claude Desktop after editing the config.
262
+
263
+ ### 6. Verify Through Your MCP Client
264
+
265
+ With the bridge still running, ask your MCP client:
266
+
267
+ ```text
268
+ List all sources in the current ParaView session.
269
+ ```
270
+
271
+ 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`.
274
+
275
+ ---
276
+
277
+ ## What Can It Control?
278
+
279
+ There are two levels of control:
280
+
281
+ 1. **Fixed MCP tools** for common workflows: scene inspection, loading data,
282
+ filters, display/coloring, camera, screenshots, data export, and animation
283
+ export.
284
+ 2. **Python execution** through `paraview_python_exec`, which can run trusted
285
+ local Python inside the ParaView bridge session. Use this for anything not
286
+ covered by a fixed tool, including arbitrary `paraview.simple` scripts.
287
+
288
+ So the fixed tool list is intentionally finite, but the Python execution tool
289
+ is the general escape hatch for the broader ParaView API.
290
+
118
291
  ---
119
292
 
120
293
  ## Example prompts
@@ -259,14 +432,14 @@ Async jobs run in a separate headless `pvpython` process via `HeadlessPvpythonEx
259
432
 
260
433
  ---
261
434
 
262
- ## Safety model
435
+ ## Python execution trust model
263
436
 
264
- - **Blocked modules** — scripts cannot import `subprocess`, `shutil`, `socket`, `ctypes`,
265
- `multiprocessing`, `webbrowser`, or several network-facing stdlib modules.
437
+ - **Trusted local execution** — `paraview_python_exec` can run arbitrary Python available to the active
438
+ ParaView Python process, including imports and full `paraview.simple` workflows.
266
439
  - **Output bounding** — stdout/stderr capped at **50 KB**.
267
440
  - **Cooperative timeout** — default 30 seconds per script execution.
268
441
  - **Script path validation** — optionally restrict execution to approved root directories.
269
- - The bridge runs inside `pvpython` with the same trust level as a local ParaView session.
442
+ - The bridge runs inside ParaView's Python runtime with the same trust level as that local session.
270
443
  - This is a local desktop automation tool — not a public API sandbox.
271
444
 
272
445
  ---
@@ -311,9 +484,11 @@ paraview-mcp-server/
311
484
  ├── bridge/
312
485
  │ ├── __init__.py
313
486
  │ ├── server.py # TCP socket bridge server
487
+ │ ├── gui_bridge.py # Non-blocking live GUI bridge lifecycle
314
488
  │ ├── command_handler.py # Command registry + paraview.simple handlers (27 commands)
315
- │ └── execution.py # python.execute helper with safety controls
489
+ │ └── execution.py # trusted local python.execute helper
316
490
  ├── scripts/
491
+ │ ├── start_paraview_gui_bridge.py
317
492
  │ ├── start_paraview_bridge.py
318
493
  │ ├── paraview_bridge_request.py
319
494
  │ └── library/ # Reusable pvpython snippets
@@ -329,7 +504,7 @@ paraview-mcp-server/
329
504
  └── tests/
330
505
  ├── test_server.py # 31 tools, connection, headless, async jobs
331
506
  ├── test_protocol.py # Wire encoding, fake bridge integration
332
- └── test_command_handler.py # All 27 handlers + safety controls
507
+ └── test_command_handler.py # All 27 handlers + execution controls
333
508
  ```
334
509
 
335
510
  ---
@@ -1,47 +1,96 @@
1
- # paraview-mcp-server
1
+ # ParaView MCP Server
2
2
 
3
3
  **Control ParaView with AI assistants through the Model Context Protocol.**
4
4
 
5
- `paraview-mcp-server` is a two-process bridge that lets AI assistants such as Claude Desktop
6
- and Codex CLI open datasets, apply filters, color data, and export screenshots in ParaView
7
- using natural language.
5
+ `paraview-mcp-python` provides an MCP server plus a ParaView-side bridge so
6
+ AI assistants such as Codex CLI and Claude Desktop can inspect a ParaView
7
+ session, open datasets, apply filters, color data, run ParaView Python, and
8
+ export screenshots.
9
+
10
+ The command installed for MCP clients is still:
11
+
12
+ ```bash
13
+ paraview-mcp-server
14
+ ```
8
15
 
9
16
  ---
10
17
 
11
- ## How it works
18
+ ## What Parts Are There?
19
+
20
+ There are three moving pieces:
21
+
22
+ | Part | Runs where | Purpose |
23
+ |---|---|---|
24
+ | **MCP client** | Codex CLI, Claude Desktop, or another MCP host | Starts the MCP server and calls tools. |
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. |
27
+
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`.
12
32
 
13
33
  ```
14
- MCP Client (Claude Desktop, Codex CLI, …)
15
- ⇅ stdio
16
- paraview-mcp-server ← thin MCP server, defines 31 tools
17
- ⇅ JSON / TCP localhost:9876
18
- ParaView bridge (pvpython) ← dispatches commands with paraview.simple
19
-
20
- paraview.simple / servermanager
34
+ ┌──────────────────────────────┐ stdio ┌────────────────────────┐
35
+ │ MCP Client │ ◄──────────────► │ MCP Server │
36
+ Codex / Claude / other host │ │ paraview-mcp-server │
37
+ └──────────────────────────────┘ └──────────┬─────────────┘
38
+ JSON/TCP
39
+ │ 127.0.0.1:9876
40
+ ┌──────────▼─────────────┐
41
+ │ ParaView Bridge │
42
+ │ live GUI process │
43
+ └──────────┬─────────────┘
44
+
45
+ ┌──────────▼─────────────┐
46
+ │ paraview.simple │
47
+ │ ParaView runtime │
48
+ └────────────────────────┘
21
49
  ```
22
50
 
23
- - The **MCP server** is a normal Python package. It speaks MCP over stdio and forwards
24
- every tool call as a JSON request to the bridge over a local TCP socket.
25
- - The **bridge** runs inside `pvpython`. It receives JSON commands, dispatches them through
26
- a command registry, calls `paraview.simple`, and returns JSON results.
27
- - Neither process depends on the other's code at import time.
28
- - A **headless pvpython executor** lets the MCP server run scripts in a separate
29
- `pvpython` process for long-running or async workflows (no bridge needed).
51
+ Why a ParaView-side bridge? ParaView's useful automation API is
52
+ `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.
55
+
56
+ For live GUI modification, use:
57
+
58
+ ```text
59
+ Codex/Claude -> MCP server -> bridge inside open ParaView GUI -> live GUI session
60
+ ```
61
+
62
+ For headless automation, use:
63
+
64
+ ```text
65
+ Codex/Claude -> MCP server -> bridge inside pvpython -> headless ParaView runtime
66
+ ```
30
67
 
31
68
  See [`docs/architecture.md`](docs/architecture.md) for a full diagram, protocol reference,
32
69
  and tool namespace table.
33
70
 
34
71
  ---
35
72
 
36
- ## Quick start
73
+ ## Install
37
74
 
38
- ### 1. Install the MCP server
75
+ ### Option A: Install the MCP server from PyPI
76
+
77
+ Use this when you only need the MCP server executable in your normal Python
78
+ environment:
39
79
 
40
80
  ```bash
41
81
  pip install paraview-mcp-python
42
82
  ```
43
83
 
44
- For local development from this repository:
84
+ This installs:
85
+
86
+ ```bash
87
+ paraview-mcp-server
88
+ ```
89
+
90
+ ### Option B: Clone this repository for the ParaView bridge
91
+
92
+ The bridge code must be available to ParaView's Python runtime. For live GUI
93
+ control and local development, clone the repository:
45
94
 
46
95
  ```bash
47
96
  git clone https://github.com/djeada/paraview-mcp-server.git
@@ -51,18 +100,112 @@ source .venv/bin/activate # Windows: .venv\Scripts\activate
51
100
  pip install -e .
52
101
  ```
53
102
 
54
- ### 2. Start the ParaView bridge
103
+ This creates:
104
+
105
+ ```bash
106
+ .venv/bin/paraview-mcp-server
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Start Everything
112
+
113
+ Start the pieces in this order.
114
+
115
+ ### 1. Start the ParaView GUI Bridge
116
+
117
+ Open ParaView, then run the GUI bridge script from the Python Shell:
118
+
119
+ 1. In ParaView, open **Tools -> Python Shell**.
120
+ 2. Click **Run Script**.
121
+ 3. Select:
122
+
123
+ ```text
124
+ /absolute/path/to/paraview-mcp-server/scripts/start_paraview_gui_bridge.py
125
+ ```
126
+
127
+ Expected output:
128
+
129
+ ```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()
139
+ ```
140
+
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
+ ```
148
+
149
+ ### 2. Optional: Start a Headless `pvpython` Bridge
55
150
 
56
- In a terminal that has `pvpython` on `PATH`:
151
+ Use this only when you do not need to modify an already-open ParaView GUI:
57
152
 
58
153
  ```bash
154
+ cd /path/to/paraview-mcp-server
59
155
  pvpython scripts/start_paraview_bridge.py
60
- # → ParaView bridge ready on 127.0.0.1:9876
61
156
  ```
62
157
 
63
- The bridge listens for JSON commands from the MCP server.
158
+ Expected output:
159
+
160
+ ```text
161
+ ParaView bridge ready on 127.0.0.1:9876
162
+ ```
163
+
164
+ Keep that terminal running. This controls the `pvpython` session, not a GUI
165
+ window opened separately.
166
+
167
+ ### 3. Verify the Bridge Directly
168
+
169
+ Before involving an MCP client, send one raw bridge command:
170
+
171
+ ```bash
172
+ python scripts/paraview_bridge_request.py scene.get_info
173
+ ```
174
+
175
+ Expected response shape:
176
+
177
+ ```json
178
+ {
179
+ "success": true,
180
+ "result": {
181
+ "source_count": 0,
182
+ "active_view_type": "RenderView"
183
+ }
184
+ }
185
+ ```
186
+
187
+ If this fails, fix the bridge before configuring Codex or Claude.
188
+
189
+ ### 4. Register the MCP Server with Codex CLI
190
+
191
+ If you installed from PyPI:
192
+
193
+ ```bash
194
+ codex mcp add paraview -- paraview-mcp-server
195
+ codex mcp list
196
+ ```
197
+
198
+ If you are using the local repository:
64
199
 
65
- ### 3. Register the MCP server with your AI client
200
+ ```bash
201
+ codex mcp add paraview -- /absolute/path/to/paraview-mcp-server/.venv/bin/paraview-mcp-server
202
+ codex mcp list
203
+ ```
204
+
205
+ Codex starts the MCP server automatically when needed. The ParaView bridge
206
+ must already be running separately.
207
+
208
+ ### 5. Register with Claude Desktop
66
209
 
67
210
  **Claude Desktop** — add to `claude_desktop_config.json`:
68
211
 
@@ -76,12 +219,42 @@ The bridge listens for JSON commands from the MCP server.
76
219
  }
77
220
  ```
78
221
 
79
- **Codex CLI:**
222
+ For a PyPI install, use the absolute path returned by:
80
223
 
81
224
  ```bash
82
- codex mcp add paraview -- /absolute/path/to/.venv/bin/paraview-mcp-server
225
+ which paraview-mcp-server
83
226
  ```
84
227
 
228
+ Restart Claude Desktop after editing the config.
229
+
230
+ ### 6. Verify Through Your MCP Client
231
+
232
+ With the bridge still running, ask your MCP client:
233
+
234
+ ```text
235
+ List all sources in the current ParaView session.
236
+ ```
237
+
238
+ 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`.
241
+
242
+ ---
243
+
244
+ ## What Can It Control?
245
+
246
+ There are two levels of control:
247
+
248
+ 1. **Fixed MCP tools** for common workflows: scene inspection, loading data,
249
+ filters, display/coloring, camera, screenshots, data export, and animation
250
+ export.
251
+ 2. **Python execution** through `paraview_python_exec`, which can run trusted
252
+ local Python inside the ParaView bridge session. Use this for anything not
253
+ covered by a fixed tool, including arbitrary `paraview.simple` scripts.
254
+
255
+ So the fixed tool list is intentionally finite, but the Python execution tool
256
+ is the general escape hatch for the broader ParaView API.
257
+
85
258
  ---
86
259
 
87
260
  ## Example prompts
@@ -226,14 +399,14 @@ Async jobs run in a separate headless `pvpython` process via `HeadlessPvpythonEx
226
399
 
227
400
  ---
228
401
 
229
- ## Safety model
402
+ ## Python execution trust model
230
403
 
231
- - **Blocked modules** — scripts cannot import `subprocess`, `shutil`, `socket`, `ctypes`,
232
- `multiprocessing`, `webbrowser`, or several network-facing stdlib modules.
404
+ - **Trusted local execution** — `paraview_python_exec` can run arbitrary Python available to the active
405
+ ParaView Python process, including imports and full `paraview.simple` workflows.
233
406
  - **Output bounding** — stdout/stderr capped at **50 KB**.
234
407
  - **Cooperative timeout** — default 30 seconds per script execution.
235
408
  - **Script path validation** — optionally restrict execution to approved root directories.
236
- - The bridge runs inside `pvpython` with the same trust level as a local ParaView session.
409
+ - The bridge runs inside ParaView's Python runtime with the same trust level as that local session.
237
410
  - This is a local desktop automation tool — not a public API sandbox.
238
411
 
239
412
  ---
@@ -278,9 +451,11 @@ paraview-mcp-server/
278
451
  ├── bridge/
279
452
  │ ├── __init__.py
280
453
  │ ├── server.py # TCP socket bridge server
454
+ │ ├── gui_bridge.py # Non-blocking live GUI bridge lifecycle
281
455
  │ ├── command_handler.py # Command registry + paraview.simple handlers (27 commands)
282
- │ └── execution.py # python.execute helper with safety controls
456
+ │ └── execution.py # trusted local python.execute helper
283
457
  ├── scripts/
458
+ │ ├── start_paraview_gui_bridge.py
284
459
  │ ├── start_paraview_bridge.py
285
460
  │ ├── paraview_bridge_request.py
286
461
  │ └── library/ # Reusable pvpython snippets
@@ -296,7 +471,7 @@ paraview-mcp-server/
296
471
  └── tests/
297
472
  ├── test_server.py # 31 tools, connection, headless, async jobs
298
473
  ├── test_protocol.py # Wire encoding, fake bridge integration
299
- └── test_command_handler.py # All 27 handlers + safety controls
474
+ └── test_command_handler.py # All 27 handlers + execution controls
300
475
  ```
301
476
 
302
477
  ---
@@ -1,9 +1,7 @@
1
1
  """Python script execution helper for the ParaView bridge.
2
2
 
3
- Safety controls
4
- ---------------
5
- - **Blocked-module import hook** prevents scripts from importing dangerous
6
- standard-library modules (``subprocess``, ``shutil``, ``socket``, …).
3
+ Execution controls
4
+ ------------------
7
5
  - **Output bounding** caps stdout/stderr at 50 KB.
8
6
  - **Timeout** (cooperative) — the caller can supply ``timeout_seconds``.
9
7
  - **Script-path execution** reads the script from disk, validating that it
@@ -12,12 +10,9 @@ Safety controls
12
10
 
13
11
  from __future__ import annotations
14
12
 
15
- import importlib.abc
16
- import importlib.machinery
17
13
  import io
18
14
  import json
19
15
  import os
20
- import sys
21
16
  import threading
22
17
  import time
23
18
  import traceback
@@ -27,20 +22,6 @@ from typing import Any
27
22
 
28
23
  MAX_OUTPUT_SIZE = 50_000
29
24
 
30
- BLOCKED_MODULES: set[str] = {
31
- "subprocess",
32
- "shutil",
33
- "socket",
34
- "ctypes",
35
- "multiprocessing",
36
- "webbrowser",
37
- "http.server",
38
- "xmlrpc.server",
39
- "ftplib",
40
- "smtplib",
41
- "telnetlib",
42
- }
43
-
44
25
  # Optionally set via bridge config; empty list → no restriction.
45
26
  APPROVED_SCRIPT_ROOTS: list[str] = []
46
27
 
@@ -50,60 +31,6 @@ ALLOW_INLINE_CODE: bool = True
50
31
  DEFAULT_TIMEOUT_SECONDS: float = 30.0
51
32
 
52
33
 
53
- # ---------------------------------------------------------------------------
54
- # Import hook that blocks dangerous modules during script execution
55
- # ---------------------------------------------------------------------------
56
-
57
-
58
- class _BlockedImportFinder(importlib.abc.MetaPathFinder):
59
- """Raises ``ImportError`` for modules in the *blocked* set."""
60
-
61
- _active = False
62
- _blocked: set[str] = set()
63
-
64
- def find_module(self, fullname: str, path=None):
65
- """Return *self* if the module should be blocked, ``None`` otherwise (legacy hook)."""
66
- if self._active and fullname in self._blocked:
67
- return self
68
- return None
69
-
70
- def find_spec(self, fullname: str, path=None, target=None):
71
- """Raise ``ImportError`` for blocked modules (modern import hook)."""
72
- if self._active and fullname in self._blocked:
73
- raise ImportError(f"Module {fullname!r} is blocked during ParaView MCP script execution")
74
- return None
75
-
76
- def load_module(self, fullname: str):
77
- raise ImportError(f"Module {fullname!r} is blocked during ParaView MCP script execution")
78
-
79
-
80
- _BLOCKER = _BlockedImportFinder()
81
-
82
-
83
- def _install_import_blocker() -> dict[str, Any]:
84
- """Activate the blocker and hide already-imported blocked modules.
85
-
86
- Returns a dict of hidden modules that must be restored later.
87
- """
88
- _BLOCKER._blocked = BLOCKED_MODULES
89
- _BLOCKER._active = True
90
- if _BLOCKER not in sys.meta_path:
91
- sys.meta_path.insert(0, _BLOCKER)
92
- # Temporarily remove blocked modules from sys.modules so `import X`
93
- # falls through to the meta-path hooks instead of hitting the cache.
94
- hidden: dict[str, Any] = {}
95
- for mod_name in BLOCKED_MODULES:
96
- if mod_name in sys.modules:
97
- hidden[mod_name] = sys.modules.pop(mod_name)
98
- return hidden
99
-
100
-
101
- def _remove_import_blocker(hidden: dict[str, Any]) -> None:
102
- _BLOCKER._active = False
103
- # Restore previously-imported modules.
104
- sys.modules.update(hidden)
105
-
106
-
107
34
  # ---------------------------------------------------------------------------
108
35
  # Helpers
109
36
  # ---------------------------------------------------------------------------
@@ -199,14 +126,11 @@ def execute_code(
199
126
  assert isinstance(code, str)
200
127
 
201
128
  def _run() -> None:
202
- hidden = _install_import_blocker()
203
129
  try:
204
130
  with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
205
131
  exec(compile(code, "<paraview-mcp-script>", "exec"), namespace) # noqa: S102
206
132
  except Exception as exc:
207
133
  result_holder["error"] = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
208
- finally:
209
- _remove_import_blocker(hidden)
210
134
 
211
135
  thread = threading.Thread(target=_run, daemon=True)
212
136
  thread.start()
@@ -0,0 +1,55 @@
1
+ """Helpers for starting the bridge from an already-open ParaView GUI session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from bridge.server import HOST, PORT, ParaViewBridgeServer
8
+
9
+ _SERVER: ParaViewBridgeServer | None = None
10
+
11
+
12
+ def start_gui_bridge(host: str = HOST, port: int = PORT) -> dict[str, Any]:
13
+ """Start the bridge inside the current ParaView Python process.
14
+
15
+ This function is intentionally non-blocking so it can be called from the
16
+ ParaView GUI Python shell without freezing the application.
17
+ """
18
+ global _SERVER
19
+ if _SERVER is not None and _SERVER.is_running:
20
+ return {
21
+ "host": _SERVER.host,
22
+ "port": _SERVER.port,
23
+ "running": True,
24
+ "already_running": True,
25
+ }
26
+
27
+ server = ParaViewBridgeServer(host=host, port=port)
28
+ server.start()
29
+ _SERVER = server
30
+ return {
31
+ "host": server.host,
32
+ "port": server.port,
33
+ "running": True,
34
+ "already_running": False,
35
+ }
36
+
37
+
38
+ def stop_gui_bridge() -> dict[str, Any]:
39
+ """Stop the bridge started by :func:`start_gui_bridge`."""
40
+ global _SERVER
41
+ if _SERVER is None:
42
+ return {"running": False, "stopped": False}
43
+
44
+ host = _SERVER.host
45
+ port = _SERVER.port
46
+ _SERVER.stop()
47
+ _SERVER = None
48
+ return {"host": host, "port": port, "running": False, "stopped": True}
49
+
50
+
51
+ def gui_bridge_status() -> dict[str, Any]:
52
+ """Return the current GUI bridge status."""
53
+ if _SERVER is None or not _SERVER.is_running:
54
+ return {"running": False}
55
+ return {"host": _SERVER.host, "port": _SERVER.port, "running": True}
@@ -1,7 +1,7 @@
1
1
  """Dependency-free bridge command parameter validation.
2
2
 
3
- The bridge runs inside ParaView's ``pvpython``, which commonly does not have
4
- the MCP server's Python dependencies installed. These classes intentionally
3
+ The bridge runs inside ParaView's Python runtime, which commonly does not have
4
+ the MCP server's Python dependencies installed. These classes intentionally
5
5
  provide the small ``model_validate(...).model_dump(...)`` surface used by the
6
6
  command handler without importing third-party packages.
7
7
  """
@@ -33,6 +33,18 @@ class ParaViewBridgeServer:
33
33
  self._handler = CommandHandler()
34
34
  self._handler_lock = threading.Lock()
35
35
 
36
+ @property
37
+ def host(self) -> str:
38
+ return self._host
39
+
40
+ @property
41
+ def port(self) -> int:
42
+ return self._port
43
+
44
+ @property
45
+ def is_running(self) -> bool:
46
+ return self._running
47
+
36
48
  def start(self):
37
49
  if self._running:
38
50
  return
@@ -40,6 +52,7 @@ class ParaViewBridgeServer:
40
52
  self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
41
53
  self._server_socket.settimeout(1.0)
42
54
  self._server_socket.bind((self._host, self._port))
55
+ self._host, self._port = self._server_socket.getsockname()[:2]
43
56
  self._server_socket.listen(5)
44
57
  self._running = True
45
58
  self._thread = threading.Thread(target=self._accept_loop, daemon=True)
@@ -19,20 +19,26 @@
19
19
  │ JSON / TCP localhost:9876
20
20
  │ (newline-delimited JSON)
21
21
  ┌──────────▼─────────────┐
22
- │ ParaView bridge │ Runs inside pvpython
23
- │ bridge/ │
22
+ │ ParaView GUI bridge │ Runs inside the open ParaView GUI
23
+ │ bridge/ │ via scripts/start_paraview_gui_bridge.py
24
24
  │ · ParaViewBridgeServer│
25
25
  │ · CommandHandler │ 27 registered commands
26
26
  │ · execute_code() │
27
27
  └──────────┬─────────────┘
28
28
 
29
29
  ┌──────────▼─────────────┐
30
- │ paraview.simple │ ParaView Python API
30
+ │ paraview.simple │ Live ParaView GUI session
31
31
  │ servermanager │
32
32
  └────────────────────────┘
33
33
  ```
34
34
 
35
- Alternative headless transport (no bridge required):
35
+ Alternative headless bridge:
36
+
37
+ ```
38
+ MCP Client → paraview-mcp-server → pvpython scripts/start_paraview_bridge.py
39
+ ```
40
+
41
+ Alternative headless script transport (no long-running bridge required):
36
42
 
37
43
  ```
38
44
  ┌────────────────────────┐
@@ -67,8 +73,9 @@ Alternative headless transport (no bridge required):
67
73
  | Module | Responsibility |
68
74
  |---|---|
69
75
  | `server.py` | Threaded TCP socket server, newline-delimited JSON framing |
76
+ | `gui_bridge.py` | Non-blocking helpers for starting/stopping the bridge inside ParaView GUI |
70
77
  | `command_handler.py` | Command registry mapping 27 command names to `paraview.simple` calls |
71
- | `execution.py` | `execute_code()` — exec-based Python execution with safety controls (blocked modules, timeout, output cap, script path validation) |
78
+ | `execution.py` | `execute_code()` — trusted local Python execution with timeout, output cap, and optional script path validation |
72
79
  | `__init__.py` | Package marker |
73
80
 
74
81
  ---
@@ -213,20 +220,33 @@ MCP client
213
220
 
214
221
  ## Lifecycle
215
222
 
216
- 1. User starts the bridge: `pvpython scripts/start_paraview_bridge.py`
223
+ 1. User starts the live GUI bridge from ParaView: **Tools → Python Shell → Run Script**,
224
+ selecting `scripts/start_paraview_gui_bridge.py`.
217
225
  2. Bridge server binds to `127.0.0.1:9876` and listens for TCP connections.
218
226
  3. User starts an MCP client (Claude Desktop, Codex CLI, etc.)
219
227
  4. MCP client spawns `paraview-mcp-server` over stdio.
220
228
  5. MCP server connects to bridge on startup (or lazy-connects on first tool call).
221
229
  6. User issues a natural language request → client calls an MCP tool → server
222
230
  forwards as JSON → bridge dispatches → returns result.
223
- 7. User stops the bridge with Ctrl+C. Server reconnects on next call if the bridge restarts.
231
+ 7. User stops the GUI bridge with `stop_gui_bridge()` in ParaView's Python Shell.
232
+ Server reconnects on next call if the bridge restarts.
224
233
 
225
234
  ---
226
235
 
227
236
  ## Configuration
228
237
 
229
- ### Bridge
238
+ ### Live GUI Bridge
239
+
240
+ Run this from ParaView's Python Shell with **Run Script**:
241
+
242
+ ```text
243
+ scripts/start_paraview_gui_bridge.py
244
+ ```
245
+
246
+ The script starts a background TCP server in the open ParaView GUI process and
247
+ returns immediately.
248
+
249
+ ### Headless Bridge
230
250
 
231
251
  ```bash
232
252
  pvpython scripts/start_paraview_bridge.py --host 127.0.0.1 --port 9876
@@ -283,7 +303,7 @@ paraview-mcp-server/
283
303
  │ ├── __init__.py
284
304
  │ ├── server.py # TCP bridge server
285
305
  │ ├── command_handler.py # 27-command registry
286
- │ └── execution.py # python.execute with safety controls
306
+ │ └── execution.py # trusted local python.execute helper
287
307
  ├── scripts/
288
308
  │ ├── start_paraview_bridge.py
289
309
  │ ├── paraview_bridge_request.py
@@ -7,7 +7,10 @@ escape hatch for workflows that require more than the fixed tool set.
7
7
 
8
8
  Two transports are supported:
9
9
 
10
- 1. **Bridge** (default) - code runs inside the running pvpython bridge process via exec().
10
+ 1. **Bridge** (default) - code runs inside the active bridge process via exec().
11
+ For live GUI control, that process is the open ParaView GUI where
12
+ `scripts/start_paraview_gui_bridge.py` was run. For headless control, it is
13
+ the `pvpython scripts/start_paraview_bridge.py` process.
11
14
  2. **Headless** - code runs in a separate pvpython subprocess via HeadlessPvpythonExecutor.
12
15
 
13
16
  ---
@@ -63,13 +66,14 @@ Headless transport adds:
63
66
 
64
67
  ---
65
68
 
66
- ## Safety model
69
+ ## Python Execution Trust Model
67
70
 
68
- ### Blocked modules
71
+ ### Trusted local execution
69
72
 
70
- The following modules are blocked during script execution:
71
- subprocess, shutil, socket, ctypes, multiprocessing, webbrowser,
72
- http.server, xmlrpc.server, ftplib, smtplib, telnetlib.
73
+ Bridge scripts run inside the local ParaView Python process and may import
74
+ normal Python modules. In live GUI mode, that is the open ParaView GUI process.
75
+ This is intentional: `paraview_python_exec` is the escape hatch for full
76
+ ParaView automation when the fixed MCP tool set is too small.
73
77
 
74
78
  ### Output bounding
75
79
 
@@ -134,5 +138,5 @@ Use paraview_python_exec_async for long-running scripts:
134
138
 
135
139
  - Cancellation token (bridge transport) for cooperative cancellation
136
140
  - Script library registry for referencing scripts by name
137
- - Sandboxed execution beyond the blocked-module list
141
+ - Optional sandboxed execution mode for deployments that need stricter isolation
138
142
  - Resource limits for memory usage per script
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "paraview-mcp-python"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "MCP server for controlling ParaView via AI assistants"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -46,7 +46,7 @@ dev = [
46
46
  ]
47
47
 
48
48
  [tool.hatch.build.targets.wheel]
49
- packages = ["src/paraview_mcp_server"]
49
+ packages = ["src/paraview_mcp_server", "bridge"]
50
50
 
51
51
  [tool.hatch.build.targets.sdist]
52
52
  include = [
@@ -0,0 +1,42 @@
1
+ """Start the MCP bridge inside an already-open ParaView GUI session.
2
+
3
+ Run from ParaView:
4
+
5
+ Tools -> Python Shell -> Run Script
6
+
7
+ Select this file. The script starts the TCP bridge in a background thread and
8
+ returns immediately, so the ParaView GUI remains usable. MCP commands will then
9
+ modify this open ParaView session.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ import sys
17
+
18
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
19
+
20
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
+ if REPO_ROOT not in sys.path:
22
+ sys.path.insert(0, REPO_ROOT)
23
+
24
+ from bridge.gui_bridge import gui_bridge_status, start_gui_bridge, stop_gui_bridge # noqa: E402
25
+
26
+ globals()["stop_gui_bridge"] = stop_gui_bridge
27
+ globals()["gui_bridge_status"] = gui_bridge_status
28
+
29
+
30
+ def main() -> None:
31
+ host = os.environ.get("PARAVIEW_MCP_HOST", "127.0.0.1")
32
+ port = int(os.environ.get("PARAVIEW_MCP_PORT", "9876"))
33
+ status = start_gui_bridge(host=host, port=port)
34
+ state = "already running" if status["already_running"] else "started"
35
+ print(f"ParaView MCP GUI bridge {state} on {status['host']}:{status['port']}")
36
+ print("Verify from a terminal with:")
37
+ print(" python scripts/paraview_bridge_request.py scene.get_info")
38
+ print("Stop from the ParaView Python Shell with:")
39
+ print(" stop_gui_bridge()")
40
+
41
+
42
+ main()
@@ -532,15 +532,16 @@ class TestPythonExecuteHandler:
532
532
  assert "timeout" in result["error"].lower()
533
533
 
534
534
 
535
- class TestExecutionSafety:
536
- """Test the safety controls in bridge/execution.py."""
535
+ class TestExecutionControls:
536
+ """Test the execution controls in bridge/execution.py."""
537
537
 
538
- def test_blocked_module_import(self):
538
+ def test_standard_library_imports_are_allowed(self):
539
539
  from bridge.execution import execute_code
540
540
 
541
- result = execute_code(code="import subprocess")
542
- assert result["error"] is not None
543
- assert "blocked" in result["error"].lower() or "subprocess" in result["error"]
541
+ result = execute_code(code="import subprocess\n__result__ = subprocess.__name__")
542
+
543
+ assert result["error"] is None
544
+ assert result["result"] == "subprocess"
544
545
 
545
546
  def test_output_capping(self):
546
547
  from bridge.execution import _cap_output
@@ -0,0 +1,55 @@
1
+ """Tests for the ParaView GUI bridge lifecycle helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ from bridge import gui_bridge
8
+
9
+
10
+ def teardown_function(_function):
11
+ gui_bridge.stop_gui_bridge()
12
+
13
+
14
+ def test_start_gui_bridge_is_non_blocking_and_reports_status():
15
+ with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
16
+ status = gui_bridge.start_gui_bridge(port=0)
17
+
18
+ assert status["running"] is True
19
+ assert status["already_running"] is False
20
+ assert status["host"] == "127.0.0.1"
21
+ assert status["port"] > 0
22
+ assert gui_bridge.gui_bridge_status() == {
23
+ "host": status["host"],
24
+ "port": status["port"],
25
+ "running": True,
26
+ }
27
+
28
+
29
+ def test_start_gui_bridge_is_idempotent():
30
+ with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
31
+ first = gui_bridge.start_gui_bridge(port=0)
32
+ second = gui_bridge.start_gui_bridge(port=0)
33
+
34
+ assert first["running"] is True
35
+ assert second == {
36
+ "host": first["host"],
37
+ "port": first["port"],
38
+ "running": True,
39
+ "already_running": True,
40
+ }
41
+
42
+
43
+ def test_stop_gui_bridge_stops_running_server():
44
+ with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
45
+ started = gui_bridge.start_gui_bridge(port=0)
46
+
47
+ stopped = gui_bridge.stop_gui_bridge()
48
+
49
+ assert stopped == {
50
+ "host": started["host"],
51
+ "port": started["port"],
52
+ "running": False,
53
+ "stopped": True,
54
+ }
55
+ assert gui_bridge.gui_bridge_status() == {"running": False}