# python-exec-mcp

MCP server for Python script execution — an alternative execution path for AI agents that eliminates shell-escape risks by design.

## Why Not Bash?

Current agentic CLI tools (Claude Code, Codex, etc.) give AI agents access to Bash via string-based permission rules. This approach has structural problems:

- **Bypass-prone permissions** — String prefix matching on commands is trivially defeated by shell operators (`&&`, `;`, `|`). Multiple CVEs have been filed against this design pattern.
- **Platform-dependent** — Shell behavior varies across OS, locale, and installed tools.
- **Overpowered by default** — A Bash tool can `rm -rf /`, `curl` arbitrary URLs, or `fly deploy` to production. Allowlisting is a losing game.

LLMs are already fluent in Python. Instead of handing them a shell and hoping the permission regex holds, give them a Python interpreter with shell-escape paths structurally removed.

See also: [Your MCP Doesn't Need 30 Tools: It Needs Code](https://lucumr.pocoo.org/2025/8/18/code-mcps/) (Armin Ronacher)

## Overview

Provides `execute`, `execute_file`, and `session_start` tools via [Model Context Protocol](https://modelcontextprotocol.io/) (stdio transport). Auto-detects `python3` or `micropython` and applies a safety preamble before every script that:

- **Removes** dangerous `os` attributes: `system`, `popen`, `exec*`, `spawn*`, `fork`, `kill`, etc.
- **Blocks** modules: `subprocess`, `shutil`, `importlib` (always), plus any in `PYEXEC_DENY`
- **Guards** `__import__` — a custom import function rejects denied modules at the language level

**This is not a security sandbox.** It does not isolate the filesystem, network, or memory (that requires a VM or container). It is a pragmatic alternative to Bash that eliminates the most common and dangerous class of agent mistakes: unintended shell command execution. Responsibility for script content lies with the caller.

## Install

```bash
cargo install python-exec-mcp
```

Requires `python3` or `micropython` on PATH.

## Usage

### Claude Code (`.mcp.json`)

```json
{
  "mcpServers": {
    "python-exec": {
      "command": "python-exec-mcp",
      "args": ["--mcp"]
    }
  }
}
```

### With ENV configuration

```json
{
  "mcpServers": {
    "python-exec": {
      "command": "python-exec-mcp",
      "args": ["--mcp"],
      "env": {
        "PYEXEC_ENGINE": "python3",
        "PYEXEC_TIMEOUT": "60",
        "PYEXEC_DENY": "urllib,http,socket",
        "PYEXEC_DENY_PATHS": "/etc:/root:~/.ssh"
      }
    }
  }
}
```

## Tools

| Tool | Description |
|------|-------------|
| `session_start` | Configure engine, timeout, workdir, deny modules, deny paths at runtime |
| `execute` | Execute a Python script string |
| `execute_file` | Execute a `.py` file |

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `PYEXEC_ENGINE` | auto (`python3` > `micropython`) | Python binary name or path |
| `PYEXEC_TIMEOUT` | `30` | Max execution time in seconds (cap: 300) |
| `PYEXEC_WORKDIR` | cwd | Default working directory |
| `PYEXEC_DENY` | (none) | Additional modules to block (comma-separated) |
| `PYEXEC_DENY_PATHS` | (none) | Paths to block from `open()` (colon-separated, prefix match) |
| `PYEXEC_RESTRICT_PATH` | `false` | When `true`/`1`, `execute_file` only allows paths within `PYEXEC_WORKDIR` |

## Design Decisions

### What this blocks

Shell-escape paths — the functions and modules that let Python act as a shell:

- `os.system()`, `os.popen()`, `os.exec*()`, `os.spawn*()`, `os.fork()`, `os.kill()`
- `subprocess`, `shutil`, `ctypes`, `multiprocessing`, `signal`
- `importlib` (prevents reloading blocked modules or bypassing `sys.modules` blocks)

### What this does NOT block

- **File I/O** (`open()`, `pathlib`) — use `PYEXEC_DENY_PATHS` to restrict specific paths
- **Network** (`socket`, `http.client`) — add to `PYEXEC_DENY` if needed
- **CPU/memory** — use `PYEXEC_TIMEOUT` for runaway scripts; OS-level limits for memory

This is intentional. The goal is to be a useful Python execution environment, not a jail. For full isolation, use a container or VM ([NVIDIA guidance](https://developer.nvidia.com/blog/practical-security-guidance-for-sandboxing-agentic-workflows-and-managing-execution-risk/)).

### Gotchas

- **Transitive imports.** Some stdlib modules import denied modules internally. For example, `import tempfile` fails because `tempfile` imports `shutil` (which is denied by default, as it contains `rmtree` etc.). This is by design.
- **`PYEXEC_DENY_PATHS` uses `realpath`.** Paths are resolved through symlinks before matching. On macOS, `/tmp` is a symlink to `/private/tmp`, so blocking `/tmp` correctly blocks `/tmp/foo` even though the resolved path is `/private/tmp/foo`.
- **`session_start` is additive.** Calling `session_start` with `deny_paths` or `deny` appends to the existing lists (does not replace).

## License

MIT OR Apache-2.0
