agent-repl-mcp 0.0.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.
@@ -0,0 +1,4 @@
1
+ .venv
2
+ __pycache__
3
+ dist
4
+ .ruff_cache
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.5
2
+ Name: agent-repl-mcp
3
+ Version: 0.0.1
4
+ Author: David Vujic
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: fastmcp
8
+ Requires-Dist: ipykernel
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Python REPL MCP
12
+ A REPL that agents can use during development.
13
+
14
+ Current status: __Work in Progress__.
@@ -0,0 +1,4 @@
1
+ # Python REPL MCP
2
+ A REPL that agents can use during development.
3
+
4
+ Current status: __Work in Progress__.
@@ -0,0 +1,2 @@
1
+ from agent_repl_mcp.agenticpytools.environment.core import is_windows, venv_python
2
+ __all__ = ['is_windows', 'venv_python']
@@ -0,0 +1,29 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ def is_windows() -> bool:
6
+ return os.name == "nt"
7
+
8
+
9
+ def windows_venv(venv_dir: Path) -> Path:
10
+ return venv_dir / "Scripts" / "python.exe"
11
+
12
+
13
+ def venv_path(venv_dir: Path) -> Path:
14
+ return windows_venv(venv_dir) if is_windows() else venv_dir / "bin" / "python"
15
+
16
+
17
+ def venv_python(project_path: str | Path) -> Path:
18
+ """Find the Python interpreter of the project's venv."""
19
+ project = Path(project_path).expanduser().resolve()
20
+
21
+ candidates = [project / ".venv"]
22
+
23
+ for venv_dir in candidates:
24
+ exe = venv_path(venv_dir)
25
+
26
+ if exe.exists():
27
+ return exe
28
+
29
+ raise FileNotFoundError(f"No virtual environment found in {project}")
@@ -0,0 +1,3 @@
1
+ from agent_repl_mcp.agenticpytools.kernel import client
2
+ from agent_repl_mcp.agenticpytools.kernel.content import capture
3
+ __all__ = ['capture', 'client']
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+ from agent_repl_mcp.agenticpytools import storage
3
+ from agent_repl_mcp.agenticpytools.kernel.close import shutdown_adopted
4
+ from agent_repl_mcp.agenticpytools.kernel.core import get_kernel_name
5
+ from agent_repl_mcp.agenticpytools.kernel.create import create_and_start_kernel
6
+ from jupyter_client import BlockingKernelClient
7
+
8
+ def create(path: str, timeout: int) -> BlockingKernelClient:
9
+ project = Path(path).expanduser().resolve()
10
+ kernel_name = get_kernel_name(project.name)
11
+ stored = storage.load_entry(kernel_name)
12
+ if stored:
13
+ try:
14
+ kc = BlockingKernelClient()
15
+ kc.load_connection_file(stored['connection_file'])
16
+ kc.start_channels()
17
+ kc.wait_for_ready(timeout=timeout)
18
+ if kc.is_alive():
19
+ return kc
20
+ else:
21
+ shutdown_adopted(stored['connection_file'])
22
+ storage.delete_entry(kernel_name)
23
+ except (FileNotFoundError, RuntimeError):
24
+ shutdown_adopted(stored['connection_file'])
25
+ storage.delete_entry(kernel_name)
26
+ km = create_and_start_kernel(project)
27
+ kc = km.client()
28
+ kc.start_channels()
29
+ kc.wait_for_ready(timeout=timeout)
30
+ if not kc.is_alive():
31
+ shutdown_adopted(str(km.connection_file))
32
+ raise RuntimeError('Failed to create or connect to kernel.')
33
+ storage.save_entry(kernel_name, str(km.connection_file), path)
34
+ return kc
35
+
36
+ def close(project: Path) -> None:
37
+ kernel_name = get_kernel_name(project.name)
38
+ stored = storage.load_entry(kernel_name)
39
+ if not stored:
40
+ print('not running')
41
+ return
42
+ shutdown_adopted(stored['connection_file'])
43
+ storage.delete_entry(kernel_name)
@@ -0,0 +1,32 @@
1
+ import time
2
+ from pathlib import Path
3
+
4
+ from jupyter_client import BlockingKernelClient
5
+
6
+
7
+ def shutdown_adopted(connection_file: str) -> None:
8
+ """Shut down a kernel we didn't spawn and clean up its files."""
9
+ kc = BlockingKernelClient()
10
+
11
+ try:
12
+ kc.load_connection_file(connection_file)
13
+ except FileNotFoundError:
14
+ return
15
+
16
+ try:
17
+ kc.start_channels()
18
+ kc.shutdown()
19
+ deadline = time.monotonic() + 10
20
+
21
+ while time.monotonic() < deadline:
22
+ if not kc.is_alive():
23
+ break
24
+
25
+ time.sleep(0.2)
26
+ finally:
27
+ kc.stop_channels()
28
+
29
+ Path(connection_file).unlink(missing_ok=True)
30
+
31
+ for f in Path(connection_file).parent.glob(Path(connection_file).stem + "-ipc-*"):
32
+ f.unlink(missing_ok=True)
@@ -0,0 +1,36 @@
1
+ from queue import Empty
2
+ from typing import Any
3
+
4
+ from jupyter_client import BlockingKernelClient
5
+
6
+
7
+ def capture(
8
+ kc: BlockingKernelClient,
9
+ msg_id: Any,
10
+ timeout: int,
11
+ ) -> tuple[list, list, dict[str, Any] | None, str | None]:
12
+ """Capture the stdout, stderr, result and error."""
13
+ stdout, stderr, result, error = [], [], None, None
14
+
15
+ while True:
16
+ try:
17
+ msg = kc.get_iopub_msg(timeout=timeout)
18
+ except Empty:
19
+ raise TimeoutError("kernel didn't finish")
20
+
21
+ if msg["parent_header"].get("msg_id") != msg_id:
22
+ continue
23
+
24
+ msg_type = msg["header"]["msg_type"]
25
+ content = msg["content"]
26
+
27
+ if msg_type == "stream":
28
+ (stdout if content["name"] == "stdout" else stderr).append(content["text"])
29
+ elif msg_type in ("display_data", "execute_result"):
30
+ result = content["data"]
31
+ elif msg_type == "error":
32
+ error = "\n".join(content["traceback"])
33
+ elif msg_type == "status" and content["execution_state"] == "idle":
34
+ break
35
+
36
+ return stdout, stderr, result, error
@@ -0,0 +1,16 @@
1
+ from pathlib import Path
2
+ from agent_repl_mcp.agenticpytools.environment import is_windows
3
+ from jupyter_core.paths import jupyter_runtime_dir
4
+
5
+ def get_kernel_name(project_name: str) -> str:
6
+ return f'repl-{project_name}'
7
+
8
+ def get_connection_file_path(kernel_id: str) -> Path:
9
+ runtime_dir = Path(jupyter_runtime_dir())
10
+ return runtime_dir / f'kernel-{kernel_id}.json'
11
+
12
+ def get_transport() -> str:
13
+ return 'tcp' if is_windows() else 'ipc'
14
+
15
+ def spec_data(argv: list[str], kernel_name: str) -> dict:
16
+ return {'argv': argv, 'display_name': f'Python REPL ({kernel_name})', 'language': 'python'}
@@ -0,0 +1,37 @@
1
+ import json
2
+ import tempfile
3
+ import uuid
4
+ from pathlib import Path
5
+ from agent_repl_mcp.agenticpytools.environment import venv_python
6
+ from agent_repl_mcp.agenticpytools.kernel.core import get_connection_file_path, get_kernel_name, get_transport, spec_data
7
+ from jupyter_client import KernelManager
8
+ from jupyter_client.kernelspec import KernelSpecManager
9
+
10
+ def create_kernel_manager(kernel_name: str, connection_file: str, **_kwargs) -> KernelManager:
11
+ transport = get_transport()
12
+ return KernelManager(kernel_name=kernel_name, connection_file=connection_file, transport=transport)
13
+
14
+ def ensure_project_spec(kernel_name: str, current_python: str) -> None:
15
+ """Install a kernelspec pointing at the project's venv, if missing."""
16
+ argv = [current_python, '-m', 'ipykernel_launcher', '-f', '{connection_file}']
17
+ ksm = KernelSpecManager()
18
+ if kernel_name in ksm.get_all_specs():
19
+ return
20
+ with tempfile.TemporaryDirectory() as tmp:
21
+ spec_dir = Path(tmp) / kernel_name
22
+ spec_dir.mkdir()
23
+ data = spec_data(argv, kernel_name)
24
+ spec_path = spec_dir / 'kernel.json'
25
+ spec_path.write_text(json.dumps(data))
26
+ ksm.install_kernel_spec(str(spec_dir), kernel_name=kernel_name, user=True)
27
+
28
+ def create_and_start_kernel(project: Path) -> KernelManager:
29
+ kernel_name = get_kernel_name(project.name)
30
+ current_python = str(venv_python(project))
31
+ ensure_project_spec(kernel_name, current_python)
32
+ transport = get_transport()
33
+ kernel_id = str(uuid.uuid4())
34
+ connection_file = str(get_connection_file_path(kernel_id))
35
+ km = KernelManager(kernel_name=kernel_name, connection_file=connection_file, transport=transport)
36
+ km.start_kernel()
37
+ return km
@@ -0,0 +1,2 @@
1
+ from agent_repl_mcp.agenticpytools.repl_mcp import core
2
+ __all__ = ['core']
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+ from agent_repl_mcp.agenticpytools.kernel import capture, client
4
+ from fastmcp import FastMCP
5
+ mcp = FastMCP('My MCP Server')
6
+ TIMEOUT = 30
7
+
8
+ def return_data(stdout: list | None=None, stderr: list | None=None, result: dict | None=None, error: str | None=None) -> dict[str, Any | None]:
9
+ return {'stdout': stdout, 'stderr': stderr, 'result': result, 'error': error}
10
+
11
+ @mcp.tool
12
+ def run(data: str, project_full_path: str) -> dict[str, Any | None]:
13
+ """Execute code in the kernel and return the output.
14
+
15
+ Returns a dictionary
16
+ stdout: from standard output
17
+ stderr: from standard error
18
+ result: from code evaluation
19
+ error: any caught errors during evaluation
20
+ """
21
+ kc = client.create(project_full_path, TIMEOUT)
22
+ msg_id = kc.execute(data)
23
+ try:
24
+ stdout, stderr, result, error = capture(kc, msg_id, TIMEOUT)
25
+ except TimeoutError:
26
+ return return_data(error='Timeout: try again.')
27
+ return return_data(stdout, stderr, result, error)
28
+
29
+ @mcp.tool
30
+ def close(project_full_path: str) -> None:
31
+ """Close the kernel."""
32
+ project = Path(project_full_path).expanduser().resolve()
33
+ client.close(project)
34
+ if __name__ == '__main__':
35
+ mcp.run()
@@ -0,0 +1,2 @@
1
+ from agent_repl_mcp.agenticpytools.storage.core import delete_entry, list_entries, load_entry, save_entry
2
+ __all__ = ['delete_entry', 'list_entries', 'load_entry', 'save_entry']
@@ -0,0 +1,14 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+ from agent_repl_mcp.agenticpytools.storage.schema import PROJECT_KERNEL_SCHEMA
4
+ DB_PATH = Path.home() / '.python-repl-mcp' / 'kernels.db'
5
+
6
+ def get_conn() -> sqlite3.Connection:
7
+ """Open a connection with sane settings. Caller owns it (use as context manager)."""
8
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
9
+ conn = sqlite3.connect(DB_PATH, timeout=10)
10
+ conn.row_factory = sqlite3.Row
11
+ conn.execute('PRAGMA journal_mode=WAL')
12
+ conn.execute('PRAGMA foreign_keys=ON')
13
+ conn.execute(PROJECT_KERNEL_SCHEMA)
14
+ return conn
@@ -0,0 +1,22 @@
1
+ """Sqlite-backed kernel registry."""
2
+ from agent_repl_mcp.agenticpytools.storage.connection import get_conn
3
+
4
+ def load_entry(kernel_name: str) -> dict | None:
5
+ with get_conn() as conn:
6
+ row = conn.execute('SELECT kernel_name, connection_file, project_path FROM kernels WHERE kernel_name = ?', (kernel_name,)).fetchone()
7
+ return dict(row) if row else None
8
+
9
+ def save_entry(kernel_name: str, connection_file: str, project_path: str) -> None:
10
+ with get_conn() as conn:
11
+ conn.execute("INSERT INTO kernels (kernel_name, connection_file, project_path)\n VALUES (?, ?, ?)\n ON CONFLICT(kernel_name) DO UPDATE SET\n connection_file = excluded.connection_file,\n project_path = excluded.project_path,\n updated_at = datetime('now')", (kernel_name, connection_file, project_path))
12
+ conn.commit()
13
+
14
+ def delete_entry(kernel_name: str) -> None:
15
+ with get_conn() as conn:
16
+ conn.execute('DELETE FROM kernels WHERE kernel_name = ?', (kernel_name,))
17
+ conn.commit()
18
+
19
+ def list_entries() -> list[dict]:
20
+ """Handy for a future 'status' tool."""
21
+ with get_conn() as conn:
22
+ return [dict(r) for r in conn.execute('SELECT * FROM kernels ORDER BY updated_at DESC')]
@@ -0,0 +1,9 @@
1
+ PROJECT_KERNEL_SCHEMA = """
2
+ CREATE TABLE IF NOT EXISTS kernels (
3
+ kernel_name TEXT PRIMARY KEY,
4
+ connection_file TEXT NOT NULL,
5
+ project_path TEXT NOT NULL,
6
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
8
+ );
9
+ """
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-polylith-bricks"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "agent-repl-mcp"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name = "David Vujic" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ repository = "https://github.com/DavidVujic/agentic-python-tools"
13
+ license = "MIT"
14
+ readme = "README.md"
15
+ dependencies = ["fastmcp", "ipykernel"]
16
+
17
+ [tool.hatch.build.targets.wheel]
18
+ packages = ["agent_repl_mcp"]
19
+
20
+ [tool.hatch.build.hooks.polylith-bricks]
21
+
22
+ [tool.polylith.build]
23
+ top-namespace = "agent_repl_mcp"
24
+
25
+ [tool.polylith.bricks]
26
+ "../../bases/agenticpytools/repl_mcp" = "agenticpytools/repl_mcp"
27
+ "../../components/agenticpytools/environment" = "agenticpytools/environment"
28
+ "../../components/agenticpytools/kernel" = "agenticpytools/kernel"
29
+ "../../components/agenticpytools/storage" = "agenticpytools/storage"