mcp-python-exec-sandbox 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.
- mcp_python_exec_sandbox-0.1.4/.github/workflows/ci.yml +61 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/CLAUDE.md +4 -3
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/PKG-INFO +46 -16
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/README.md +45 -15
- mcp_python_exec_sandbox-0.1.4/e2e_tests/test_multi_version.py +68 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_sandbox_enforcement.py +0 -36
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/pyproject.toml +1 -1
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/__main__.py +12 -2
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/config.py +1 -1
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/executor.py +29 -3
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/sandbox.py +26 -25
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_config.py +4 -1
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_sandbox.py +11 -24
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/uv.lock +1 -1
- mcp_python_exec_sandbox-0.1.2/.github/workflows/ci.yml +0 -25
- mcp_python_exec_sandbox-0.1.2/profiles/sandbox_macos.sb +0 -39
- mcp_python_exec_sandbox-0.1.2/src/mcp_python_exec_sandbox/sandbox_macos.py +0 -92
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/.devcontainer/Dockerfile +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/.devcontainer/devcontainer.json +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/.gitignore +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/LICENSE +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/__init__.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_data_science.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_docker_sandbox.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_mcp_protocol.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_package_install.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_real_execution.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/profiles/Dockerfile +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/profiles/warmup_packages.txt +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/__init__.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/cache.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/errors.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/output.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/sandbox_docker.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/sandbox_linux.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/script.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/server.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/__init__.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/conftest.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_executor.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_integration.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_output.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_script.py +0 -0
- {mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/tests/test_server.py +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v4
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.13"
|
|
17
|
+
- run: uv sync --dev
|
|
18
|
+
- run: uv run ruff check .
|
|
19
|
+
- run: uv run ruff format --check .
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: astral-sh/setup-uv@v4
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.13"
|
|
28
|
+
- run: uv sync --dev
|
|
29
|
+
- run: uv run pytest tests/ -v
|
|
30
|
+
|
|
31
|
+
e2e-native:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: astral-sh/setup-uv@v4
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.13"
|
|
38
|
+
- run: sudo apt-get update && sudo apt-get install -y bubblewrap
|
|
39
|
+
- run: uv sync --dev
|
|
40
|
+
- run: uv run pytest e2e_tests/test_sandbox_enforcement.py -v
|
|
41
|
+
|
|
42
|
+
e2e-docker:
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/checkout@v4
|
|
46
|
+
- uses: astral-sh/setup-uv@v4
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.13"
|
|
49
|
+
- run: docker build -t mcp-python-exec-sandbox profiles/
|
|
50
|
+
- run: uv sync --dev
|
|
51
|
+
- run: uv run pytest e2e_tests/test_docker_sandbox.py -v
|
|
52
|
+
|
|
53
|
+
e2e-multi-version:
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
- uses: astral-sh/setup-uv@v4
|
|
58
|
+
with:
|
|
59
|
+
python-version: "3.13"
|
|
60
|
+
- run: uv sync --dev
|
|
61
|
+
- run: uv run pytest e2e_tests/test_multi_version.py -v
|
|
@@ -18,11 +18,12 @@ src/mcp_python_exec_sandbox/ # Package source
|
|
|
18
18
|
executor.py # uv subprocess orchestration
|
|
19
19
|
script.py # PEP 723 metadata parsing/merging
|
|
20
20
|
sandbox.py # Sandbox ABC + factory
|
|
21
|
-
|
|
21
|
+
sandbox_linux.py # bubblewrap sandbox (Linux)
|
|
22
|
+
sandbox_docker.py # Docker sandbox (macOS/any)
|
|
22
23
|
config.py, cache.py, output.py, errors.py
|
|
23
24
|
tests/ # Unit + integration tests (mocked or local uv)
|
|
24
25
|
e2e_tests/ # End-to-end tests (require uv + network)
|
|
25
|
-
profiles/ # Dockerfile,
|
|
26
|
+
profiles/ # Dockerfile, warmup packages
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
## Commands
|
|
@@ -40,7 +41,7 @@ uv run pytest e2e_tests/ -v # E2E tests (slow, needs network)
|
|
|
40
41
|
- Lint with `uv run ruff check .` and format with `uv run ruff format --check .` before committing. Fix issues with `--fix` / `ruff format .`.
|
|
41
42
|
- Tool docstrings in `server.py` are user-facing — they become the MCP tool descriptions that agents see. Write them for an LLM audience: include examples, avoid unexplained jargon, link PEPs.
|
|
42
43
|
- Always pin versions in examples (e.g. `"pandas>=2.2"` not `"pandas"`).
|
|
43
|
-
- Sandbox backends must degrade gracefully: if the tool (bwrap,
|
|
44
|
+
- Sandbox backends must degrade gracefully: if the tool (bwrap, docker) is missing, fall back to `NoopSandbox` with a warning. Native sandbox is Linux-only (bwrap); macOS defaults to Docker.
|
|
44
45
|
|
|
45
46
|
## Contribution format
|
|
46
47
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-python-exec-sandbox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: MCP server for secure Python script execution with automatic dependency management
|
|
5
5
|
Project-URL: Homepage, https://github.com/lu-zhengda/mcp-python-exec-sandbox
|
|
6
6
|
Project-URL: Repository, https://github.com/lu-zhengda/mcp-python-exec-sandbox
|
|
@@ -29,7 +29,7 @@ Sandboxed Python execution for AI agents. Scripts run in ephemeral, isolated env
|
|
|
29
29
|
|
|
30
30
|
Every coding agent can already run Python on your host. The problem is what happens next: packages accumulate, venvs sprawl, and a rogue `pip install` breaks your system. **mcp-python-exec-sandbox** eliminates this:
|
|
31
31
|
|
|
32
|
-
- Scripts execute in a sandbox (bubblewrap on Linux,
|
|
32
|
+
- Scripts execute in a sandbox (bubblewrap on Linux, Docker on macOS/other platforms)
|
|
33
33
|
- Dependencies are declared inline and resolved ephemerally via `uv`
|
|
34
34
|
- Nothing touches your host's Python, site-packages, or virtualenvs
|
|
35
35
|
- Each execution is isolated and disposable
|
|
@@ -56,25 +56,24 @@ Additional requirements depend on your chosen sandbox backend:
|
|
|
56
56
|
| Setup | Additional requirements | Install |
|
|
57
57
|
|-------|------------------------|---------|
|
|
58
58
|
| **Native sandbox (Linux)** | [bubblewrap](https://github.com/containers/bubblewrap) | `sudo apt install bubblewrap` |
|
|
59
|
-
| **
|
|
60
|
-
| **Docker sandbox** | [Docker Engine](https://docs.docker.com/engine/install/) | See Docker docs |
|
|
59
|
+
| **Docker sandbox (macOS, any)** | [Docker Engine](https://docs.docker.com/engine/install/) | See Docker docs |
|
|
61
60
|
| **No sandbox** | None | -- |
|
|
62
61
|
|
|
63
62
|
> **Host Python vs. execution Python:** These are independent. Python 3.13+ is needed to run the server process itself. The `--python-version` flag controls which Python version your *scripts* execute on -- uv downloads the target version automatically. You do not need to install Python 3.14 or 3.15 on your host to run scripts on those versions.
|
|
64
63
|
|
|
65
64
|
## Quick start
|
|
66
65
|
|
|
67
|
-
### Claude Code (native sandbox
|
|
66
|
+
### Claude Code (Linux -- native sandbox)
|
|
68
67
|
|
|
69
68
|
```bash
|
|
70
|
-
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
69
|
+
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
71
70
|
```
|
|
72
71
|
|
|
73
|
-
### Claude Code (Docker sandbox)
|
|
72
|
+
### Claude Code (macOS -- Docker sandbox, recommended)
|
|
74
73
|
|
|
75
74
|
```bash
|
|
76
75
|
docker build -t mcp-python-exec-sandbox profiles/
|
|
77
|
-
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
76
|
+
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
78
77
|
```
|
|
79
78
|
|
|
80
79
|
> The Docker image build requires the repo source. Clone it first: `git clone https://github.com/lu-zhengda/mcp-python-exec-sandbox.git`
|
|
@@ -85,14 +84,45 @@ claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox --sandbox-backend d
|
|
|
85
84
|
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox --sandbox-backend none
|
|
86
85
|
```
|
|
87
86
|
|
|
88
|
-
###
|
|
87
|
+
### Cursor
|
|
88
|
+
|
|
89
|
+
Add to `.cursor/mcp.json` (project-level) or `~/.cursor/mcp.json` (global):
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"python-sandbox": {
|
|
95
|
+
"command": "uvx",
|
|
96
|
+
"args": ["mcp-python-exec-sandbox"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### OpenAI Codex CLI
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
codex mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Or add to `.codex/config.toml`:
|
|
109
|
+
|
|
110
|
+
```toml
|
|
111
|
+
[mcp_servers.python-sandbox]
|
|
112
|
+
command = "uvx"
|
|
113
|
+
args = ["mcp-python-exec-sandbox"]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Other MCP clients
|
|
117
|
+
|
|
118
|
+
Any client that supports the MCP stdio transport can use this server:
|
|
89
119
|
|
|
90
120
|
```json
|
|
91
121
|
{
|
|
92
122
|
"mcpServers": {
|
|
93
123
|
"python-sandbox": {
|
|
94
124
|
"command": "uvx",
|
|
95
|
-
"args": ["mcp-python-exec-sandbox"
|
|
125
|
+
"args": ["mcp-python-exec-sandbox"]
|
|
96
126
|
}
|
|
97
127
|
}
|
|
98
128
|
}
|
|
@@ -166,11 +196,10 @@ Validates a script's PEP 723 metadata and dependencies without executing it.
|
|
|
166
196
|
| Backend | Platform | Tool | Notes |
|
|
167
197
|
|---------|----------|------|-------|
|
|
168
198
|
| `native` | Linux | bubblewrap | Namespace isolation, network allowed |
|
|
169
|
-
| `native` | macOS | sandbox-exec | Seatbelt profiles, network allowed |
|
|
170
199
|
| `docker` | Any | Docker | Container isolation, resource limits |
|
|
171
200
|
| `none` | Any | -- | No sandboxing (not recommended) |
|
|
172
201
|
|
|
173
|
-
If the
|
|
202
|
+
The default backend is `native` (bubblewrap) on Linux and `docker` on macOS/other platforms. Specifying `--sandbox-backend native` on macOS automatically redirects to Docker. If the sandbox tool is unavailable, the server falls back to `none` with a warning.
|
|
174
203
|
|
|
175
204
|
### Docker sandbox setup
|
|
176
205
|
|
|
@@ -185,7 +214,7 @@ mcp-python-exec-sandbox [OPTIONS]
|
|
|
185
214
|
|
|
186
215
|
Options:
|
|
187
216
|
--python-version TEXT Python version for execution (default: 3.13)
|
|
188
|
-
--sandbox-backend TEXT native | docker | none (default: native)
|
|
217
|
+
--sandbox-backend TEXT native | docker | none (default: native on Linux, docker on macOS)
|
|
189
218
|
--max-timeout INT Maximum allowed timeout in seconds (default: 300)
|
|
190
219
|
--default-timeout INT Default timeout in seconds (default: 30)
|
|
191
220
|
--max-output-bytes INT Maximum output size in bytes (default: 102400)
|
|
@@ -211,11 +240,12 @@ src/mcp_python_exec_sandbox/ # Package source
|
|
|
211
240
|
executor.py # uv subprocess orchestration
|
|
212
241
|
script.py # PEP 723 metadata parsing/merging
|
|
213
242
|
sandbox.py # Sandbox ABC + factory
|
|
214
|
-
|
|
243
|
+
sandbox_linux.py # bubblewrap sandbox (Linux)
|
|
244
|
+
sandbox_docker.py # Docker sandbox (macOS/any)
|
|
215
245
|
config.py, cache.py, output.py, errors.py
|
|
216
246
|
tests/ # Unit + integration tests (mocked or local uv)
|
|
217
247
|
e2e_tests/ # End-to-end tests (require uv + network)
|
|
218
|
-
profiles/ # Dockerfile,
|
|
248
|
+
profiles/ # Dockerfile, warmup packages
|
|
219
249
|
.devcontainer/ # Devcontainer for Linux sandbox testing from macOS
|
|
220
250
|
```
|
|
221
251
|
|
|
@@ -300,7 +330,7 @@ devcontainer exec --workspace-folder . uv run pytest e2e_tests/test_sandbox_enfo
|
|
|
300
330
|
- Add tests for new functionality: unit tests in `tests/`, E2E in `e2e_tests/` if it needs real execution.
|
|
301
331
|
- Keep dependencies minimal. Do not add runtime deps without strong justification.
|
|
302
332
|
- Tool docstrings in `server.py` are user-facing MCP tool descriptions. Write them for an LLM audience.
|
|
303
|
-
- Sandbox backends must degrade gracefully: if the tool is missing, fall back to `NoopSandbox` with a warning.
|
|
333
|
+
- Sandbox backends must degrade gracefully: if the required tool (bwrap, docker) is missing, fall back to `NoopSandbox` with a warning.
|
|
304
334
|
|
|
305
335
|
## License
|
|
306
336
|
|
|
@@ -6,7 +6,7 @@ Sandboxed Python execution for AI agents. Scripts run in ephemeral, isolated env
|
|
|
6
6
|
|
|
7
7
|
Every coding agent can already run Python on your host. The problem is what happens next: packages accumulate, venvs sprawl, and a rogue `pip install` breaks your system. **mcp-python-exec-sandbox** eliminates this:
|
|
8
8
|
|
|
9
|
-
- Scripts execute in a sandbox (bubblewrap on Linux,
|
|
9
|
+
- Scripts execute in a sandbox (bubblewrap on Linux, Docker on macOS/other platforms)
|
|
10
10
|
- Dependencies are declared inline and resolved ephemerally via `uv`
|
|
11
11
|
- Nothing touches your host's Python, site-packages, or virtualenvs
|
|
12
12
|
- Each execution is isolated and disposable
|
|
@@ -33,25 +33,24 @@ Additional requirements depend on your chosen sandbox backend:
|
|
|
33
33
|
| Setup | Additional requirements | Install |
|
|
34
34
|
|-------|------------------------|---------|
|
|
35
35
|
| **Native sandbox (Linux)** | [bubblewrap](https://github.com/containers/bubblewrap) | `sudo apt install bubblewrap` |
|
|
36
|
-
| **
|
|
37
|
-
| **Docker sandbox** | [Docker Engine](https://docs.docker.com/engine/install/) | See Docker docs |
|
|
36
|
+
| **Docker sandbox (macOS, any)** | [Docker Engine](https://docs.docker.com/engine/install/) | See Docker docs |
|
|
38
37
|
| **No sandbox** | None | -- |
|
|
39
38
|
|
|
40
39
|
> **Host Python vs. execution Python:** These are independent. Python 3.13+ is needed to run the server process itself. The `--python-version` flag controls which Python version your *scripts* execute on -- uv downloads the target version automatically. You do not need to install Python 3.14 or 3.15 on your host to run scripts on those versions.
|
|
41
40
|
|
|
42
41
|
## Quick start
|
|
43
42
|
|
|
44
|
-
### Claude Code (native sandbox
|
|
43
|
+
### Claude Code (Linux -- native sandbox)
|
|
45
44
|
|
|
46
45
|
```bash
|
|
47
|
-
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
46
|
+
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
### Claude Code (Docker sandbox)
|
|
49
|
+
### Claude Code (macOS -- Docker sandbox, recommended)
|
|
51
50
|
|
|
52
51
|
```bash
|
|
53
52
|
docker build -t mcp-python-exec-sandbox profiles/
|
|
54
|
-
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
53
|
+
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
55
54
|
```
|
|
56
55
|
|
|
57
56
|
> The Docker image build requires the repo source. Clone it first: `git clone https://github.com/lu-zhengda/mcp-python-exec-sandbox.git`
|
|
@@ -62,14 +61,45 @@ claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox --sandbox-backend d
|
|
|
62
61
|
claude mcp add python-sandbox -- uvx mcp-python-exec-sandbox --sandbox-backend none
|
|
63
62
|
```
|
|
64
63
|
|
|
65
|
-
###
|
|
64
|
+
### Cursor
|
|
65
|
+
|
|
66
|
+
Add to `.cursor/mcp.json` (project-level) or `~/.cursor/mcp.json` (global):
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"python-sandbox": {
|
|
72
|
+
"command": "uvx",
|
|
73
|
+
"args": ["mcp-python-exec-sandbox"]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### OpenAI Codex CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
codex mcp add python-sandbox -- uvx mcp-python-exec-sandbox
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or add to `.codex/config.toml`:
|
|
86
|
+
|
|
87
|
+
```toml
|
|
88
|
+
[mcp_servers.python-sandbox]
|
|
89
|
+
command = "uvx"
|
|
90
|
+
args = ["mcp-python-exec-sandbox"]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Other MCP clients
|
|
94
|
+
|
|
95
|
+
Any client that supports the MCP stdio transport can use this server:
|
|
66
96
|
|
|
67
97
|
```json
|
|
68
98
|
{
|
|
69
99
|
"mcpServers": {
|
|
70
100
|
"python-sandbox": {
|
|
71
101
|
"command": "uvx",
|
|
72
|
-
"args": ["mcp-python-exec-sandbox"
|
|
102
|
+
"args": ["mcp-python-exec-sandbox"]
|
|
73
103
|
}
|
|
74
104
|
}
|
|
75
105
|
}
|
|
@@ -143,11 +173,10 @@ Validates a script's PEP 723 metadata and dependencies without executing it.
|
|
|
143
173
|
| Backend | Platform | Tool | Notes |
|
|
144
174
|
|---------|----------|------|-------|
|
|
145
175
|
| `native` | Linux | bubblewrap | Namespace isolation, network allowed |
|
|
146
|
-
| `native` | macOS | sandbox-exec | Seatbelt profiles, network allowed |
|
|
147
176
|
| `docker` | Any | Docker | Container isolation, resource limits |
|
|
148
177
|
| `none` | Any | -- | No sandboxing (not recommended) |
|
|
149
178
|
|
|
150
|
-
If the
|
|
179
|
+
The default backend is `native` (bubblewrap) on Linux and `docker` on macOS/other platforms. Specifying `--sandbox-backend native` on macOS automatically redirects to Docker. If the sandbox tool is unavailable, the server falls back to `none` with a warning.
|
|
151
180
|
|
|
152
181
|
### Docker sandbox setup
|
|
153
182
|
|
|
@@ -162,7 +191,7 @@ mcp-python-exec-sandbox [OPTIONS]
|
|
|
162
191
|
|
|
163
192
|
Options:
|
|
164
193
|
--python-version TEXT Python version for execution (default: 3.13)
|
|
165
|
-
--sandbox-backend TEXT native | docker | none (default: native)
|
|
194
|
+
--sandbox-backend TEXT native | docker | none (default: native on Linux, docker on macOS)
|
|
166
195
|
--max-timeout INT Maximum allowed timeout in seconds (default: 300)
|
|
167
196
|
--default-timeout INT Default timeout in seconds (default: 30)
|
|
168
197
|
--max-output-bytes INT Maximum output size in bytes (default: 102400)
|
|
@@ -188,11 +217,12 @@ src/mcp_python_exec_sandbox/ # Package source
|
|
|
188
217
|
executor.py # uv subprocess orchestration
|
|
189
218
|
script.py # PEP 723 metadata parsing/merging
|
|
190
219
|
sandbox.py # Sandbox ABC + factory
|
|
191
|
-
|
|
220
|
+
sandbox_linux.py # bubblewrap sandbox (Linux)
|
|
221
|
+
sandbox_docker.py # Docker sandbox (macOS/any)
|
|
192
222
|
config.py, cache.py, output.py, errors.py
|
|
193
223
|
tests/ # Unit + integration tests (mocked or local uv)
|
|
194
224
|
e2e_tests/ # End-to-end tests (require uv + network)
|
|
195
|
-
profiles/ # Dockerfile,
|
|
225
|
+
profiles/ # Dockerfile, warmup packages
|
|
196
226
|
.devcontainer/ # Devcontainer for Linux sandbox testing from macOS
|
|
197
227
|
```
|
|
198
228
|
|
|
@@ -277,7 +307,7 @@ devcontainer exec --workspace-folder . uv run pytest e2e_tests/test_sandbox_enfo
|
|
|
277
307
|
- Add tests for new functionality: unit tests in `tests/`, E2E in `e2e_tests/` if it needs real execution.
|
|
278
308
|
- Keep dependencies minimal. Do not add runtime deps without strong justification.
|
|
279
309
|
- Tool docstrings in `server.py` are user-facing MCP tool descriptions. Write them for an LLM audience.
|
|
280
|
-
- Sandbox backends must degrade gracefully: if the tool is missing, fall back to `NoopSandbox` with a warning.
|
|
310
|
+
- Sandbox backends must degrade gracefully: if the required tool (bwrap, docker) is missing, fall back to `NoopSandbox` with a warning.
|
|
281
311
|
|
|
282
312
|
## License
|
|
283
313
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""E2E tests for multi-version Python execution."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from mcp_python_exec_sandbox.executor import execute
|
|
10
|
+
|
|
11
|
+
pytestmark = pytest.mark.skipif(
|
|
12
|
+
shutil.which("uv") is None,
|
|
13
|
+
reason="uv not installed",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize("python_version", ["3.13", "3.14"])
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_version_matches(python_version: str):
|
|
20
|
+
"""Test that the script runs on the requested Python version."""
|
|
21
|
+
script = """\
|
|
22
|
+
import sys
|
|
23
|
+
print(f"{sys.version_info.major}.{sys.version_info.minor}")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
with tempfile.TemporaryDirectory(prefix="mcp-e2e-") as tmpdir:
|
|
27
|
+
script_path = Path(tmpdir) / "test.py"
|
|
28
|
+
script_path.write_text(script)
|
|
29
|
+
|
|
30
|
+
result = await execute(
|
|
31
|
+
script_path=script_path,
|
|
32
|
+
python_version=python_version,
|
|
33
|
+
timeout=60,
|
|
34
|
+
sandbox=None,
|
|
35
|
+
max_output_bytes=102400,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
assert result.exit_code == 0, f"stderr: {result.stderr}"
|
|
39
|
+
assert result.stdout.strip() == python_version
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("python_version", ["3.13", "3.14"])
|
|
43
|
+
@pytest.mark.asyncio
|
|
44
|
+
async def test_dependency_install_across_versions(python_version: str):
|
|
45
|
+
"""Test that PEP 723 dependencies work on each Python version."""
|
|
46
|
+
script = """\
|
|
47
|
+
# /// script
|
|
48
|
+
# dependencies = ["pydantic>=2.0"]
|
|
49
|
+
# ///
|
|
50
|
+
|
|
51
|
+
import pydantic
|
|
52
|
+
print(f"pydantic {pydantic.__version__}")
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
with tempfile.TemporaryDirectory(prefix="mcp-e2e-") as tmpdir:
|
|
56
|
+
script_path = Path(tmpdir) / "test.py"
|
|
57
|
+
script_path.write_text(script)
|
|
58
|
+
|
|
59
|
+
result = await execute(
|
|
60
|
+
script_path=script_path,
|
|
61
|
+
python_version=python_version,
|
|
62
|
+
timeout=120,
|
|
63
|
+
sandbox=None,
|
|
64
|
+
max_output_bytes=102400,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert result.exit_code == 0, f"stderr: {result.stderr}"
|
|
68
|
+
assert "pydantic 2." in result.stdout
|
|
@@ -18,42 +18,6 @@ pytestmark = [
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only")
|
|
22
|
-
@pytest.mark.skipif(
|
|
23
|
-
shutil.which("sandbox-exec") is None,
|
|
24
|
-
reason="sandbox-exec not found",
|
|
25
|
-
)
|
|
26
|
-
@pytest.mark.asyncio
|
|
27
|
-
async def test_macos_sandbox_blocks_home_read():
|
|
28
|
-
"""Test that macOS sandbox blocks reading files outside allowed dirs."""
|
|
29
|
-
sandbox = get_sandbox("native")
|
|
30
|
-
|
|
31
|
-
script = """\
|
|
32
|
-
import os
|
|
33
|
-
try:
|
|
34
|
-
with open(os.path.expanduser("~/.ssh/id_rsa")) as f:
|
|
35
|
-
print(f.read())
|
|
36
|
-
print("ACCESS_GRANTED")
|
|
37
|
-
except (PermissionError, FileNotFoundError, OSError) as e:
|
|
38
|
-
print(f"ACCESS_DENIED: {e}")
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
with tempfile.TemporaryDirectory(prefix="mcp-e2e-") as tmpdir:
|
|
42
|
-
script_path = Path(tmpdir) / "test.py"
|
|
43
|
-
script_path.write_text(script)
|
|
44
|
-
|
|
45
|
-
result = await execute(
|
|
46
|
-
script_path=script_path,
|
|
47
|
-
python_version="3.13",
|
|
48
|
-
timeout=30,
|
|
49
|
-
sandbox=sandbox,
|
|
50
|
-
max_output_bytes=102400,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Should not have been able to read the file
|
|
54
|
-
assert "ACCESS_GRANTED" not in result.stdout
|
|
55
|
-
|
|
56
|
-
|
|
57
21
|
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
|
|
58
22
|
@pytest.mark.skipif(
|
|
59
23
|
shutil.which("bwrap") is None,
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _default_sandbox_backend() -> str:
|
|
10
|
+
"""Return the platform-appropriate default sandbox backend."""
|
|
11
|
+
return "native" if sys.platform == "linux" else "docker"
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
15
|
+
default_backend = _default_sandbox_backend()
|
|
9
16
|
parser = argparse.ArgumentParser(
|
|
10
17
|
prog="mcp-python-exec-sandbox",
|
|
11
18
|
description="MCP server for secure Python script execution",
|
|
@@ -18,8 +25,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
|
18
25
|
parser.add_argument(
|
|
19
26
|
"--sandbox-backend",
|
|
20
27
|
choices=["native", "docker", "none"],
|
|
21
|
-
default=
|
|
22
|
-
help=
|
|
28
|
+
default=default_backend,
|
|
29
|
+
help=(
|
|
30
|
+
"Sandbox backend: native (bwrap, Linux only), docker, or none "
|
|
31
|
+
f"(default: {default_backend})"
|
|
32
|
+
),
|
|
23
33
|
)
|
|
24
34
|
parser.add_argument(
|
|
25
35
|
"--max-timeout",
|
|
@@ -10,7 +10,7 @@ class ServerConfig:
|
|
|
10
10
|
"""Configuration for the MCP Python executor server."""
|
|
11
11
|
|
|
12
12
|
python_version: str = "3.13"
|
|
13
|
-
sandbox_backend: str = "native" # "native" | "docker" | "none"
|
|
13
|
+
sandbox_backend: str = "native" # "native" (Linux) | "docker" | "none"
|
|
14
14
|
max_timeout: int = 300
|
|
15
15
|
default_timeout: int = 30
|
|
16
16
|
max_output_bytes: int = 102_400 # 100KB
|
|
@@ -4,11 +4,34 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
7
9
|
import time
|
|
8
10
|
from pathlib import Path
|
|
9
11
|
|
|
10
12
|
from mcp_python_exec_sandbox.output import ExecutionResult
|
|
11
13
|
|
|
14
|
+
_uv_cache_dir: str | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_uv_cache_dir() -> str | None:
|
|
18
|
+
"""Discover uv's cache directory (cached after first call)."""
|
|
19
|
+
global _uv_cache_dir # noqa: PLW0603
|
|
20
|
+
if _uv_cache_dir is not None:
|
|
21
|
+
return _uv_cache_dir
|
|
22
|
+
uv = shutil.which("uv")
|
|
23
|
+
if uv:
|
|
24
|
+
try:
|
|
25
|
+
result = subprocess.run(
|
|
26
|
+
[uv, "cache", "dir"], capture_output=True, text=True, timeout=5
|
|
27
|
+
)
|
|
28
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
29
|
+
_uv_cache_dir = str(Path(result.stdout.strip()).resolve())
|
|
30
|
+
return _uv_cache_dir
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
return None
|
|
34
|
+
|
|
12
35
|
|
|
13
36
|
def _build_clean_env(uv_cache_dir: str | None = None) -> dict[str, str]:
|
|
14
37
|
"""Build a clean environment dict, stripping secrets from the parent env."""
|
|
@@ -48,12 +71,14 @@ async def execute(
|
|
|
48
71
|
Returns:
|
|
49
72
|
ExecutionResult with stdout, stderr, exit_code, duration, and timeout status.
|
|
50
73
|
"""
|
|
51
|
-
|
|
74
|
+
# Resolve symlinks so paths match sandbox allow-lists (e.g. /var -> /private/var)
|
|
75
|
+
resolved_path = script_path.resolve()
|
|
76
|
+
cmd = [uv_path, "run", "--script", "--python", python_version, str(resolved_path)]
|
|
52
77
|
|
|
53
78
|
if sandbox is not None:
|
|
54
|
-
cmd = sandbox.wrap(cmd,
|
|
79
|
+
cmd = sandbox.wrap(cmd, resolved_path)
|
|
55
80
|
|
|
56
|
-
env = _build_clean_env()
|
|
81
|
+
env = _build_clean_env(uv_cache_dir=_get_uv_cache_dir())
|
|
57
82
|
start = time.monotonic()
|
|
58
83
|
timed_out = False
|
|
59
84
|
|
|
@@ -63,6 +88,7 @@ async def execute(
|
|
|
63
88
|
stdout=asyncio.subprocess.PIPE,
|
|
64
89
|
stderr=asyncio.subprocess.PIPE,
|
|
65
90
|
env=env,
|
|
91
|
+
cwd=resolved_path.parent,
|
|
66
92
|
)
|
|
67
93
|
stdout_bytes, stderr_bytes = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
68
94
|
except TimeoutError:
|
|
@@ -47,6 +47,19 @@ class NoopSandbox(Sandbox):
|
|
|
47
47
|
return "none (no sandboxing)"
|
|
48
48
|
|
|
49
49
|
|
|
50
|
+
def _get_docker_sandbox() -> Sandbox:
|
|
51
|
+
"""Create a Docker sandbox, falling back to NoopSandbox if unavailable."""
|
|
52
|
+
from mcp_python_exec_sandbox.sandbox_docker import DockerSandbox
|
|
53
|
+
|
|
54
|
+
sb = DockerSandbox()
|
|
55
|
+
if not sb.is_available():
|
|
56
|
+
import logging
|
|
57
|
+
|
|
58
|
+
logging.warning("Docker not available, falling back to no sandbox")
|
|
59
|
+
return NoopSandbox()
|
|
60
|
+
return sb
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
def get_sandbox(backend: str) -> Sandbox:
|
|
51
64
|
"""Create a sandbox instance for the given backend.
|
|
52
65
|
|
|
@@ -61,35 +74,23 @@ def get_sandbox(backend: str) -> Sandbox:
|
|
|
61
74
|
from mcp_python_exec_sandbox.sandbox_linux import BubblewrapSandbox
|
|
62
75
|
|
|
63
76
|
sb = BubblewrapSandbox()
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
sb = SandboxExecSandbox()
|
|
68
|
-
else:
|
|
69
|
-
import logging
|
|
77
|
+
if not sb.is_available():
|
|
78
|
+
import logging
|
|
70
79
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
return NoopSandbox()
|
|
80
|
+
logging.warning("bwrap not found, falling back to no sandbox")
|
|
81
|
+
return NoopSandbox()
|
|
82
|
+
return sb
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
# Native sandbox is only supported on Linux; use Docker on other platforms.
|
|
85
|
+
import logging
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
logging.info(
|
|
88
|
+
"Native sandbox is Linux-only; using Docker sandbox on %s.",
|
|
89
|
+
sys.platform,
|
|
90
|
+
)
|
|
91
|
+
return _get_docker_sandbox()
|
|
83
92
|
|
|
84
93
|
if backend == "docker":
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
sb = DockerSandbox()
|
|
88
|
-
if not sb.is_available():
|
|
89
|
-
import logging
|
|
90
|
-
|
|
91
|
-
logging.warning("Docker not available, falling back to no sandbox")
|
|
92
|
-
return NoopSandbox()
|
|
93
|
-
return sb
|
|
94
|
+
return _get_docker_sandbox()
|
|
94
95
|
|
|
95
96
|
raise ValueError(f"Unknown sandbox backend: {backend!r}")
|
|
@@ -56,9 +56,12 @@ class TestServerConfig:
|
|
|
56
56
|
|
|
57
57
|
class TestParseArgs:
|
|
58
58
|
def test_defaults(self):
|
|
59
|
+
import sys
|
|
60
|
+
|
|
59
61
|
args = parse_args([])
|
|
60
62
|
assert args.python_version == "3.13"
|
|
61
|
-
|
|
63
|
+
expected_backend = "native" if sys.platform == "linux" else "docker"
|
|
64
|
+
assert args.sandbox_backend == expected_backend
|
|
62
65
|
assert args.max_timeout == 300
|
|
63
66
|
assert args.default_timeout == 30
|
|
64
67
|
assert args.max_output_bytes == 102_400
|
|
@@ -40,7 +40,17 @@ class TestGetSandbox:
|
|
|
40
40
|
assert isinstance(sb, NoopSandbox)
|
|
41
41
|
|
|
42
42
|
@patch("sys.platform", "darwin")
|
|
43
|
-
def
|
|
43
|
+
def test_native_macos_uses_docker(self):
|
|
44
|
+
"""On macOS, 'native' redirects to Docker sandbox."""
|
|
45
|
+
from mcp_python_exec_sandbox.sandbox_docker import DockerSandbox
|
|
46
|
+
|
|
47
|
+
with patch("shutil.which", return_value="/usr/local/bin/docker"):
|
|
48
|
+
sb = get_sandbox("native")
|
|
49
|
+
assert isinstance(sb, DockerSandbox)
|
|
50
|
+
|
|
51
|
+
@patch("sys.platform", "darwin")
|
|
52
|
+
def test_native_macos_falls_back_when_docker_missing(self):
|
|
53
|
+
"""On macOS, 'native' falls back to NoopSandbox if Docker is unavailable."""
|
|
44
54
|
with patch("shutil.which", return_value=None):
|
|
45
55
|
sb = get_sandbox("native")
|
|
46
56
|
assert isinstance(sb, NoopSandbox)
|
|
@@ -68,29 +78,6 @@ class TestBubblewrapSandbox:
|
|
|
68
78
|
assert wrapped[-6:] == cmd
|
|
69
79
|
|
|
70
80
|
|
|
71
|
-
class TestSandboxExecSandbox:
|
|
72
|
-
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only")
|
|
73
|
-
def test_wrap_command(self):
|
|
74
|
-
from mcp_python_exec_sandbox.sandbox_macos import SandboxExecSandbox
|
|
75
|
-
|
|
76
|
-
with patch("shutil.which", return_value="/usr/bin/sandbox-exec"):
|
|
77
|
-
sb = SandboxExecSandbox()
|
|
78
|
-
cmd = ["uv", "run", "--script", "--python", "3.13", "/tmp/test.py"]
|
|
79
|
-
wrapped = sb.wrap(cmd, Path("/tmp/test.py"))
|
|
80
|
-
assert wrapped[0] == "/usr/bin/sandbox-exec"
|
|
81
|
-
assert "-p" in wrapped
|
|
82
|
-
# Original command should be at the end
|
|
83
|
-
assert wrapped[-6:] == cmd
|
|
84
|
-
|
|
85
|
-
@pytest.mark.skipif(sys.platform != "darwin", reason="macOS only")
|
|
86
|
-
def test_describe(self):
|
|
87
|
-
from mcp_python_exec_sandbox.sandbox_macos import SandboxExecSandbox
|
|
88
|
-
|
|
89
|
-
with patch("shutil.which", return_value="/usr/bin/sandbox-exec"):
|
|
90
|
-
sb = SandboxExecSandbox()
|
|
91
|
-
assert "sandbox-exec" in sb.describe()
|
|
92
|
-
|
|
93
|
-
|
|
94
81
|
class TestDockerSandbox:
|
|
95
82
|
def test_wrap_command(self):
|
|
96
83
|
from mcp_python_exec_sandbox.sandbox_docker import DockerSandbox
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
lint:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
- uses: astral-sh/setup-uv@v4
|
|
15
|
-
- run: uv sync --dev
|
|
16
|
-
- run: uv run ruff check .
|
|
17
|
-
- run: uv run ruff format --check .
|
|
18
|
-
|
|
19
|
-
test:
|
|
20
|
-
runs-on: ubuntu-latest
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v4
|
|
23
|
-
- uses: astral-sh/setup-uv@v4
|
|
24
|
-
- run: uv sync --dev
|
|
25
|
-
- run: uv run pytest tests/ -v
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
(version 1)
|
|
2
|
-
(deny default)
|
|
3
|
-
|
|
4
|
-
;; Allow reading system libraries and frameworks
|
|
5
|
-
(allow file-read*
|
|
6
|
-
(subpath "/usr/lib")
|
|
7
|
-
(subpath "/usr/local")
|
|
8
|
-
(subpath "/System")
|
|
9
|
-
(subpath "/Library/Frameworks")
|
|
10
|
-
(subpath "/opt/homebrew")
|
|
11
|
-
(subpath "/private/tmp")
|
|
12
|
-
(subpath "/private/var/folders")
|
|
13
|
-
(subpath "/dev")
|
|
14
|
-
(subpath "{{CACHE_DIR}}")
|
|
15
|
-
(subpath "{{SCRIPT_DIR}}"))
|
|
16
|
-
|
|
17
|
-
;; Allow writing to cache and script dir
|
|
18
|
-
(allow file-write*
|
|
19
|
-
(subpath "{{CACHE_DIR}}")
|
|
20
|
-
(subpath "{{SCRIPT_DIR}}")
|
|
21
|
-
(subpath "/private/tmp")
|
|
22
|
-
(subpath "/private/var/folders"))
|
|
23
|
-
|
|
24
|
-
;; Allow process operations
|
|
25
|
-
(allow process-exec*)
|
|
26
|
-
(allow process-fork)
|
|
27
|
-
|
|
28
|
-
;; Allow network access (needed for package downloads and data-fetching scripts)
|
|
29
|
-
(allow network*)
|
|
30
|
-
|
|
31
|
-
;; Allow sysctl reads (needed by Python runtime)
|
|
32
|
-
(allow sysctl-read)
|
|
33
|
-
|
|
34
|
-
;; Allow mach lookups (needed for DNS resolution, etc.)
|
|
35
|
-
(allow mach-lookup)
|
|
36
|
-
|
|
37
|
-
;; Allow IPC operations (needed by some Python extensions)
|
|
38
|
-
(allow ipc-posix-shm-read-data)
|
|
39
|
-
(allow ipc-posix-shm-write-data)
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""macOS sandbox-exec (Seatbelt) sandbox implementation."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import shutil
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from mcp_python_exec_sandbox.sandbox import Sandbox
|
|
9
|
-
|
|
10
|
-
_PROFILE_DIR = Path(__file__).parent.parent.parent / "profiles"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SandboxExecSandbox(Sandbox):
|
|
14
|
-
"""macOS sandbox using sandbox-exec with Seatbelt profiles."""
|
|
15
|
-
|
|
16
|
-
def __init__(self) -> None:
|
|
17
|
-
self._sandbox_exec_path = shutil.which("sandbox-exec")
|
|
18
|
-
self._uv_cache_dir = Path.home() / "Library" / "Caches" / "uv"
|
|
19
|
-
self._profile_path = _PROFILE_DIR / "sandbox_macos.sb"
|
|
20
|
-
|
|
21
|
-
def is_available(self) -> bool:
|
|
22
|
-
return self._sandbox_exec_path is not None
|
|
23
|
-
|
|
24
|
-
def _build_profile(self, script_dir: str) -> str:
|
|
25
|
-
"""Build a Seatbelt profile string with the given script directory."""
|
|
26
|
-
cache_dir = str(self._uv_cache_dir)
|
|
27
|
-
|
|
28
|
-
# If the profile file exists, read and substitute parameters
|
|
29
|
-
if self._profile_path.exists():
|
|
30
|
-
template = self._profile_path.read_text()
|
|
31
|
-
return template.replace("{{SCRIPT_DIR}}", script_dir).replace(
|
|
32
|
-
"{{CACHE_DIR}}", cache_dir
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
# Fallback inline profile
|
|
36
|
-
return f"""\
|
|
37
|
-
(version 1)
|
|
38
|
-
(deny default)
|
|
39
|
-
|
|
40
|
-
;; Allow reading system libraries and frameworks
|
|
41
|
-
(allow file-read*
|
|
42
|
-
(subpath "/usr/lib")
|
|
43
|
-
(subpath "/usr/local")
|
|
44
|
-
(subpath "/System")
|
|
45
|
-
(subpath "/Library/Frameworks")
|
|
46
|
-
(subpath "/opt/homebrew")
|
|
47
|
-
(subpath "/private/tmp")
|
|
48
|
-
(subpath "/private/var/folders")
|
|
49
|
-
(subpath "/dev")
|
|
50
|
-
(subpath "{cache_dir}")
|
|
51
|
-
(subpath "{script_dir}"))
|
|
52
|
-
|
|
53
|
-
;; Allow writing to cache and script dir
|
|
54
|
-
(allow file-write*
|
|
55
|
-
(subpath "{cache_dir}")
|
|
56
|
-
(subpath "{script_dir}")
|
|
57
|
-
(subpath "/private/tmp")
|
|
58
|
-
(subpath "/private/var/folders"))
|
|
59
|
-
|
|
60
|
-
;; Allow process operations
|
|
61
|
-
(allow process-exec*)
|
|
62
|
-
(allow process-fork)
|
|
63
|
-
|
|
64
|
-
;; Allow network access
|
|
65
|
-
(allow network*)
|
|
66
|
-
|
|
67
|
-
;; Allow sysctl reads (needed by Python)
|
|
68
|
-
(allow sysctl-read)
|
|
69
|
-
|
|
70
|
-
;; Allow mach lookups (needed for DNS, etc.)
|
|
71
|
-
(allow mach-lookup)
|
|
72
|
-
|
|
73
|
-
;; Allow ipc operations
|
|
74
|
-
(allow ipc-posix-shm-read-data)
|
|
75
|
-
(allow ipc-posix-shm-write-data)
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
def wrap(self, cmd: list[str], script_path: Path) -> list[str]:
|
|
79
|
-
script_dir = str(script_path.parent)
|
|
80
|
-
profile = self._build_profile(script_dir)
|
|
81
|
-
|
|
82
|
-
return [
|
|
83
|
-
self._sandbox_exec_path or "sandbox-exec",
|
|
84
|
-
"-p",
|
|
85
|
-
profile,
|
|
86
|
-
*cmd,
|
|
87
|
-
]
|
|
88
|
-
|
|
89
|
-
def describe(self) -> str:
|
|
90
|
-
if self._sandbox_exec_path:
|
|
91
|
-
return f"sandbox-exec ({self._sandbox_exec_path})"
|
|
92
|
-
return "sandbox-exec (not found)"
|
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/.devcontainer/devcontainer.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_data_science.py
RENAMED
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_docker_sandbox.py
RENAMED
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_mcp_protocol.py
RENAMED
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_package_install.py
RENAMED
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/e2e_tests/test_real_execution.py
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/profiles/warmup_packages.txt
RENAMED
|
File without changes
|
|
File without changes
|
{mcp_python_exec_sandbox-0.1.2 → mcp_python_exec_sandbox-0.1.4}/src/mcp_python_exec_sandbox/cache.py
RENAMED
|
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
|