paraview-mcp-python 0.1.0__tar.gz → 0.1.1__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.0 → paraview_mcp_python-0.1.1}/PKG-INFO +6 -6
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/README.md +5 -5
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/bridge/execution.py +2 -78
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/docs/architecture.md +2 -2
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/docs/python-execute-design.md +6 -6
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/pyproject.toml +1 -1
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/tests/test_command_handler.py +7 -6
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/.gitignore +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/LICENSE +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/bridge/__init__.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/bridge/command_handler.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/bridge/models.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/bridge/server.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/library/color_by.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/library/create_contour.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/library/create_slice.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/library/open_dataset.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/library/reset_camera.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/library/save_screenshot.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/paraview_bridge_request.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/scripts/start_paraview_bridge.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/src/paraview_mcp_server/__init__.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/src/paraview_mcp_server/headless.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/src/paraview_mcp_server/server.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/tests/__init__.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/tests/test_bridge_server.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/tests/test_protocol.py +0 -0
- {paraview_mcp_python-0.1.0 → paraview_mcp_python-0.1.1}/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.1
|
|
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
|
|
@@ -259,10 +259,10 @@ Async jobs run in a separate headless `pvpython` process via `HeadlessPvpythonEx
|
|
|
259
259
|
|
|
260
260
|
---
|
|
261
261
|
|
|
262
|
-
##
|
|
262
|
+
## Python execution trust model
|
|
263
263
|
|
|
264
|
-
- **
|
|
265
|
-
|
|
264
|
+
- **Trusted local execution** — `paraview_python_exec` can run arbitrary Python available to `pvpython`,
|
|
265
|
+
including imports and full `paraview.simple` workflows.
|
|
266
266
|
- **Output bounding** — stdout/stderr capped at **50 KB**.
|
|
267
267
|
- **Cooperative timeout** — default 30 seconds per script execution.
|
|
268
268
|
- **Script path validation** — optionally restrict execution to approved root directories.
|
|
@@ -312,7 +312,7 @@ paraview-mcp-server/
|
|
|
312
312
|
│ ├── __init__.py
|
|
313
313
|
│ ├── server.py # TCP socket bridge server
|
|
314
314
|
│ ├── command_handler.py # Command registry + paraview.simple handlers (27 commands)
|
|
315
|
-
│ └── execution.py # python.execute helper
|
|
315
|
+
│ └── execution.py # trusted local python.execute helper
|
|
316
316
|
├── scripts/
|
|
317
317
|
│ ├── start_paraview_bridge.py
|
|
318
318
|
│ ├── paraview_bridge_request.py
|
|
@@ -329,7 +329,7 @@ paraview-mcp-server/
|
|
|
329
329
|
└── tests/
|
|
330
330
|
├── test_server.py # 31 tools, connection, headless, async jobs
|
|
331
331
|
├── test_protocol.py # Wire encoding, fake bridge integration
|
|
332
|
-
└── test_command_handler.py # All 27 handlers +
|
|
332
|
+
└── test_command_handler.py # All 27 handlers + execution controls
|
|
333
333
|
```
|
|
334
334
|
|
|
335
335
|
---
|
|
@@ -226,10 +226,10 @@ Async jobs run in a separate headless `pvpython` process via `HeadlessPvpythonEx
|
|
|
226
226
|
|
|
227
227
|
---
|
|
228
228
|
|
|
229
|
-
##
|
|
229
|
+
## Python execution trust model
|
|
230
230
|
|
|
231
|
-
- **
|
|
232
|
-
|
|
231
|
+
- **Trusted local execution** — `paraview_python_exec` can run arbitrary Python available to `pvpython`,
|
|
232
|
+
including imports and full `paraview.simple` workflows.
|
|
233
233
|
- **Output bounding** — stdout/stderr capped at **50 KB**.
|
|
234
234
|
- **Cooperative timeout** — default 30 seconds per script execution.
|
|
235
235
|
- **Script path validation** — optionally restrict execution to approved root directories.
|
|
@@ -279,7 +279,7 @@ paraview-mcp-server/
|
|
|
279
279
|
│ ├── __init__.py
|
|
280
280
|
│ ├── server.py # TCP socket bridge server
|
|
281
281
|
│ ├── command_handler.py # Command registry + paraview.simple handlers (27 commands)
|
|
282
|
-
│ └── execution.py # python.execute helper
|
|
282
|
+
│ └── execution.py # trusted local python.execute helper
|
|
283
283
|
├── scripts/
|
|
284
284
|
│ ├── start_paraview_bridge.py
|
|
285
285
|
│ ├── paraview_bridge_request.py
|
|
@@ -296,7 +296,7 @@ paraview-mcp-server/
|
|
|
296
296
|
└── tests/
|
|
297
297
|
├── test_server.py # 31 tools, connection, headless, async jobs
|
|
298
298
|
├── test_protocol.py # Wire encoding, fake bridge integration
|
|
299
|
-
└── test_command_handler.py # All 27 handlers +
|
|
299
|
+
└── test_command_handler.py # All 27 handlers + execution controls
|
|
300
300
|
```
|
|
301
301
|
|
|
302
302
|
---
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"""Python script execution helper for the ParaView bridge.
|
|
2
2
|
|
|
3
|
-
|
|
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()
|
|
@@ -68,7 +68,7 @@ Alternative headless transport (no bridge required):
|
|
|
68
68
|
|---|---|
|
|
69
69
|
| `server.py` | Threaded TCP socket server, newline-delimited JSON framing |
|
|
70
70
|
| `command_handler.py` | Command registry mapping 27 command names to `paraview.simple` calls |
|
|
71
|
-
| `execution.py` | `execute_code()` —
|
|
71
|
+
| `execution.py` | `execute_code()` — trusted local Python execution with timeout, output cap, and optional script path validation |
|
|
72
72
|
| `__init__.py` | Package marker |
|
|
73
73
|
|
|
74
74
|
---
|
|
@@ -283,7 +283,7 @@ paraview-mcp-server/
|
|
|
283
283
|
│ ├── __init__.py
|
|
284
284
|
│ ├── server.py # TCP bridge server
|
|
285
285
|
│ ├── command_handler.py # 27-command registry
|
|
286
|
-
│ └── execution.py # python.execute
|
|
286
|
+
│ └── execution.py # trusted local python.execute helper
|
|
287
287
|
├── scripts/
|
|
288
288
|
│ ├── start_paraview_bridge.py
|
|
289
289
|
│ ├── paraview_bridge_request.py
|
|
@@ -63,13 +63,13 @@ Headless transport adds:
|
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
-
##
|
|
66
|
+
## Python Execution Trust Model
|
|
67
67
|
|
|
68
|
-
###
|
|
68
|
+
### Trusted local execution
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
Bridge scripts run inside the local `pvpython` process and may import normal
|
|
71
|
+
Python modules. This is intentional: `paraview_python_exec` is the escape hatch
|
|
72
|
+
for full ParaView automation when the fixed MCP tool set is too small.
|
|
73
73
|
|
|
74
74
|
### Output bounding
|
|
75
75
|
|
|
@@ -134,5 +134,5 @@ Use paraview_python_exec_async for long-running scripts:
|
|
|
134
134
|
|
|
135
135
|
- Cancellation token (bridge transport) for cooperative cancellation
|
|
136
136
|
- Script library registry for referencing scripts by name
|
|
137
|
-
-
|
|
137
|
+
- Optional sandboxed execution mode for deployments that need stricter isolation
|
|
138
138
|
- Resource limits for memory usage per script
|
|
@@ -532,15 +532,16 @@ class TestPythonExecuteHandler:
|
|
|
532
532
|
assert "timeout" in result["error"].lower()
|
|
533
533
|
|
|
534
534
|
|
|
535
|
-
class
|
|
536
|
-
"""Test the
|
|
535
|
+
class TestExecutionControls:
|
|
536
|
+
"""Test the execution controls in bridge/execution.py."""
|
|
537
537
|
|
|
538
|
-
def
|
|
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
|
-
|
|
543
|
-
assert
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|