mcp-python-repl 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,142 @@
1
+ """
2
+ Session management for mcp-python-repl.
3
+
4
+ Each session holds an isolated Python namespace and execution history.
5
+ Sessions are identified by an opaque ``session_id`` (UUID) and expire
6
+ after a configurable TTL.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ import uuid
13
+ from dataclasses import dataclass, field
14
+ from datetime import datetime, timedelta, timezone
15
+ from typing import Any
16
+
17
+ from .config import Config
18
+
19
+
20
+ @dataclass
21
+ class ExecutionRecord:
22
+ """A single code-execution record for audit / debug."""
23
+
24
+ timestamp: str
25
+ code_preview: str
26
+ status: str
27
+ new_vars: list[str] = field(default_factory=list)
28
+ modified_vars: list[str] = field(default_factory=list)
29
+ error: str | None = None
30
+
31
+
32
+ @dataclass
33
+ class Session:
34
+ """An isolated Python execution session."""
35
+
36
+ session_id: str
37
+ created_at: datetime
38
+ last_used: datetime
39
+ namespace: dict[str, Any] = field(default_factory=dict)
40
+ history: list[ExecutionRecord] = field(default_factory=list)
41
+
42
+ # ------------------------------------------------------------------
43
+ def touch(self) -> None:
44
+ self.last_used = datetime.now(timezone.utc)
45
+
46
+ def is_expired(self, ttl_minutes: int) -> bool:
47
+ return datetime.now(timezone.utc) - self.last_used > timedelta(minutes=ttl_minutes)
48
+
49
+ def variable_summary(self) -> dict[str, str]:
50
+ """Return {name: type_name} for every user variable."""
51
+ return {k: type(v).__name__ for k, v in self.namespace.items() if not k.startswith("_")}
52
+
53
+
54
+ class SessionManager:
55
+ """Thread-safe session store with automatic eviction."""
56
+
57
+ def __init__(self, config: Config) -> None:
58
+ self._config = config
59
+ self._sessions: dict[str, Session] = {}
60
+ self._lock = threading.Lock()
61
+
62
+ # ------------------------------------------------------------------
63
+ # Public API
64
+ # ------------------------------------------------------------------
65
+
66
+ def create_session(self) -> Session:
67
+ """Create a new session, evicting stale ones first."""
68
+ self._evict_expired()
69
+
70
+ with self._lock:
71
+ if len(self._sessions) >= self._config.max_sessions:
72
+ # Evict the oldest session
73
+ oldest_id = min(
74
+ self._sessions,
75
+ key=lambda sid: self._sessions[sid].last_used,
76
+ )
77
+ del self._sessions[oldest_id]
78
+
79
+ now = datetime.now(timezone.utc)
80
+ session = Session(
81
+ session_id=uuid.uuid4().hex[:12],
82
+ created_at=now,
83
+ last_used=now,
84
+ )
85
+ self._sessions[session.session_id] = session
86
+ return session
87
+
88
+ def get_session(self, session_id: str) -> Session | None:
89
+ """Retrieve a session by id (returns *None* if expired / missing)."""
90
+ with self._lock:
91
+ session = self._sessions.get(session_id)
92
+ if session is None:
93
+ return None
94
+ if session.is_expired(self._config.session_ttl_minutes):
95
+ del self._sessions[session_id]
96
+ return None
97
+ session.touch()
98
+ return session
99
+
100
+ def get_or_create(self, session_id: str | None) -> Session:
101
+ """Return existing session or create a new one."""
102
+ if session_id:
103
+ session = self.get_session(session_id)
104
+ if session is not None:
105
+ return session
106
+ return self.create_session()
107
+
108
+ def delete_session(self, session_id: str) -> bool:
109
+ with self._lock:
110
+ return self._sessions.pop(session_id, None) is not None
111
+
112
+ def list_sessions(self) -> list[dict[str, Any]]:
113
+ self._evict_expired()
114
+ with self._lock:
115
+ return [
116
+ {
117
+ "session_id": s.session_id,
118
+ "created_at": s.created_at.isoformat(),
119
+ "last_used": s.last_used.isoformat(),
120
+ "variable_count": len(s.variable_summary()),
121
+ "history_count": len(s.history),
122
+ }
123
+ for s in self._sessions.values()
124
+ ]
125
+
126
+ @property
127
+ def count(self) -> int:
128
+ return len(self._sessions)
129
+
130
+ # ------------------------------------------------------------------
131
+ # Internals
132
+ # ------------------------------------------------------------------
133
+
134
+ def _evict_expired(self) -> None:
135
+ with self._lock:
136
+ expired = [
137
+ sid
138
+ for sid, s in self._sessions.items()
139
+ if s.is_expired(self._config.session_ttl_minutes)
140
+ ]
141
+ for sid in expired:
142
+ del self._sessions[sid]
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-python-repl
3
+ Version: 0.1.0
4
+ Summary: A production-grade MCP server providing a persistent Python REPL with multi-session support, sandboxing, and timeout protection.
5
+ Project-URL: Homepage, https://github.com/soufiane-aazizi/mcp-python-repl
6
+ Project-URL: Repository, https://github.com/soufiane-aazizi/mcp-python-repl
7
+ Project-URL: Issues, https://github.com/soufiane-aazizi/mcp-python-repl/issues
8
+ Author: Soufiane Aazizi
9
+ License-Expression: MIT
10
+ Keywords: agent,interpreter,llm,mcp,model-context-protocol,python,repl
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Interpreters
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: mcp>=1.7.0
23
+ Requires-Dist: pydantic>=2.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
26
+ Requires-Dist: pytest>=8.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.8; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ๐Ÿ mcp-python-repl
31
+
32
+ A **production-grade** MCP server providing a persistent Python REPL with multi-session support, sandboxing, and timeout protection.
33
+
34
+ Built for LLM agents that need to execute Python code across multiple turns with **variables that persist between calls**.
35
+
36
+ ## โœจ Features
37
+
38
+ | Feature | Description |
39
+ |---|---|
40
+ | **Multi-session** | Isolated sessions with unique IDs โ€” run parallel workflows |
41
+ | **Persistent namespace** | Variables survive across calls within a session |
42
+ | **Timeout protection** | Configurable execution timeout (SIGALRM on Unix) |
43
+ | **Sandboxing** | Optional mode blocks dangerous modules (`subprocess`, `socket`, etc.) |
44
+ | **Package install** | Install pip packages on-the-fly (prefers `uv` for speed) |
45
+ | **File execution** | Run `.py` files inside the persistent session |
46
+ | **Dual transport** | stdio (local) and streamable-http (remote) |
47
+ | **Full introspection** | List variables, get history, check server status |
48
+ | **Env-based config** | All settings via `REPL_*` environment variables |
49
+
50
+ ## ๐Ÿš€ Quick Start
51
+
52
+ ### With Claude Desktop / Cursor (stdio)
53
+
54
+ Add to your MCP config:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "python-repl": {
60
+ "command": "uvx",
61
+ "args": ["mcp-python-repl"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### With uv (local dev)
68
+
69
+ ```bash
70
+ # Clone and run
71
+ git clone https://github.com/soufiane-aazizi/mcp-python-repl.git
72
+ cd mcp-python-repl
73
+ uv run mcp-python-repl
74
+ ```
75
+
76
+ ### HTTP transport (remote / multi-client)
77
+
78
+ ```bash
79
+ REPL_TRANSPORT=streamable-http REPL_PORT=8000 uv run mcp-python-repl
80
+ ```
81
+
82
+ ## ๐Ÿ› ๏ธ Tools
83
+
84
+ ### Code Execution
85
+
86
+ | Tool | Description |
87
+ |---|---|
88
+ | `repl_run_code` | Execute Python code with persistent namespace |
89
+ | `repl_run_file` | Execute a `.py` file in the session |
90
+ | `repl_install_package` | Install a pip package (uses `uv` if available) |
91
+
92
+ ### Namespace Management
93
+
94
+ | Tool | Description |
95
+ |---|---|
96
+ | `repl_list_namespace` | List all variables in a session |
97
+ | `repl_get_variable` | Get the full value of a variable |
98
+ | `repl_set_variable` | Inject a variable from JSON |
99
+ | `repl_delete_variable` | Delete a specific variable |
100
+ | `repl_clear_namespace` | Clear all variables in a session |
101
+
102
+ ### Session Management
103
+
104
+ | Tool | Description |
105
+ |---|---|
106
+ | `repl_list_sessions` | List all active sessions |
107
+ | `repl_delete_session` | Delete a session and its data |
108
+
109
+ ### Debugging
110
+
111
+ | Tool | Description |
112
+ |---|---|
113
+ | `repl_get_history` | Get execution history for a session |
114
+ | `repl_server_status` | Server config, Python version, session count |
115
+
116
+ ## ๐Ÿ”„ How Persistence Works
117
+
118
+ ```
119
+ Call 1: repl_run_code(code="data = [1,2,3]; total = sum(data); result = total")
120
+ โ†’ returns: {"result": 6, "session_id": "a1b2c3d4e5f6", "new_variables": ["data", "total"]}
121
+
122
+ Call 2: repl_run_code(code="doubled = [x*2 for x in data]; result = doubled", session_id="a1b2c3d4e5f6")
123
+ โ†’ returns: {"result": [2,4,6], "new_variables": ["doubled"]}
124
+ ```
125
+
126
+ > **Important:** The `result` variable is for returning output to the caller. It does **NOT** persist. Use named variables instead.
127
+
128
+ ## โš™๏ธ Configuration
129
+
130
+ All settings are configurable via environment variables:
131
+
132
+ | Variable | Default | Description |
133
+ |---|---|---|
134
+ | `REPL_TIMEOUT` | `30` | Max execution time in seconds |
135
+ | `REPL_MAX_SESSIONS` | `50` | Maximum concurrent sessions |
136
+ | `REPL_SESSION_TTL` | `120` | Session expiry in minutes |
137
+ | `REPL_MAX_OUTPUT` | `1048576` | Max stdout/stderr capture (bytes) |
138
+ | `REPL_SANDBOX` | `false` | Enable sandboxing (`true`/`false`) |
139
+ | `REPL_TRANSPORT` | `stdio` | Transport: `stdio` or `streamable-http` |
140
+ | `REPL_HOST` | `127.0.0.1` | HTTP host (when using HTTP transport) |
141
+ | `REPL_PORT` | `8000` | HTTP port (when using HTTP transport) |
142
+ | `REPL_WORKDIR` | `cwd` | Working directory for executions |
143
+
144
+ ### Sandbox Mode
145
+
146
+ When `REPL_SANDBOX=true`, the following modules are blocked:
147
+
148
+ `subprocess`, `shutil`, `ctypes`, `socket`, `http.server`, `xmlrpc`, `ftplib`, `smtplib`, `telnetlib`, `webbrowser`
149
+
150
+ And the following builtins are removed: `exec`, `eval`, `compile`, `__import__` (replaced with a restricted version).
151
+
152
+ ## ๐Ÿงช Development
153
+
154
+ ```bash
155
+ # Install dev dependencies
156
+ uv sync --extra dev
157
+
158
+ # Run tests
159
+ uv run pytest -v
160
+
161
+ # Lint
162
+ uv run ruff check src/ tests/
163
+
164
+ # Test with MCP Inspector
165
+ npx @modelcontextprotocol/inspector uv run mcp-python-repl
166
+ ```
167
+
168
+ ## ๐Ÿ“ฆ Project Structure
169
+
170
+ ```
171
+ mcp-python-repl/
172
+ โ”œโ”€โ”€ src/mcp_python_repl/
173
+ โ”‚ โ”œโ”€โ”€ __init__.py # Package metadata
174
+ โ”‚ โ”œโ”€โ”€ config.py # Env-based configuration
175
+ โ”‚ โ”œโ”€โ”€ session.py # Multi-session manager with TTL
176
+ โ”‚ โ”œโ”€โ”€ executor.py # Python code executor (timeout + sandbox)
177
+ โ”‚ โ””โ”€โ”€ server.py # MCP server with all tools
178
+ โ”œโ”€โ”€ tests/
179
+ โ”‚ โ””โ”€โ”€ test_core.py # Unit + integration tests
180
+ โ”œโ”€โ”€ pyproject.toml # uv/hatch project config
181
+ โ”œโ”€โ”€ LICENSE # MIT
182
+ โ””โ”€โ”€ README.md
183
+ ```
184
+
185
+ ## ๐Ÿ“„ License
186
+
187
+ MIT โ€” See [LICENSE](LICENSE).
@@ -0,0 +1,8 @@
1
+ mcp_python_repl/config.py,sha256=V1bcy6z8fcTUMzKlhZSPDE2wAguG9Ug32b79BdCCXMg,2570
2
+ mcp_python_repl/executor.py,sha256=uFfw4P6fzAuE4zgKM6VEy82voQslGU3zxI1rrvOAsrw,7575
3
+ mcp_python_repl/server.py,sha256=Shnug32hOh31Qr7TQjnxVrPmg-VxaJGMOhV4m3rn9qQ,21998
4
+ mcp_python_repl/session.py,sha256=GkljC4OtmHojF51grUDYYJ5NAhEW00zPUQmtTPUxmNQ,4671
5
+ mcp_python_repl-0.1.0.dist-info/METADATA,sha256=S_UJIR7zgFeJJV6l334yf3N7oD5P4iObAxnJB0uzPlI,6309
6
+ mcp_python_repl-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ mcp_python_repl-0.1.0.dist-info/entry_points.txt,sha256=DHFkUiJlWbQ8SDZpEvCaH1EvOfeuJcDwJ1rWKwTvMww,64
8
+ mcp_python_repl-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-python-repl = mcp_python_repl.server:main