agent-repl-mcp 0.0.1__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.
- agent_repl_mcp/agenticpytools/environment/__init__.py +2 -0
- agent_repl_mcp/agenticpytools/environment/core.py +29 -0
- agent_repl_mcp/agenticpytools/kernel/__init__.py +3 -0
- agent_repl_mcp/agenticpytools/kernel/client.py +43 -0
- agent_repl_mcp/agenticpytools/kernel/close.py +32 -0
- agent_repl_mcp/agenticpytools/kernel/content.py +36 -0
- agent_repl_mcp/agenticpytools/kernel/core.py +16 -0
- agent_repl_mcp/agenticpytools/kernel/create.py +37 -0
- agent_repl_mcp/agenticpytools/repl_mcp/__init__.py +2 -0
- agent_repl_mcp/agenticpytools/repl_mcp/core.py +35 -0
- agent_repl_mcp/agenticpytools/storage/__init__.py +2 -0
- agent_repl_mcp/agenticpytools/storage/connection.py +14 -0
- agent_repl_mcp/agenticpytools/storage/core.py +22 -0
- agent_repl_mcp/agenticpytools/storage/schema.py +9 -0
- agent_repl_mcp-0.0.1.dist-info/METADATA +14 -0
- agent_repl_mcp-0.0.1.dist-info/RECORD +17 -0
- agent_repl_mcp-0.0.1.dist-info/WHEEL +4 -0
|
@@ -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,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,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,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,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,17 @@
|
|
|
1
|
+
agent_repl_mcp/agenticpytools/environment/__init__.py,sha256=xcfyyceW0BXtE3VahYBsVZaSSLF9hVDLaPuXZ6N732Q,122
|
|
2
|
+
agent_repl_mcp/agenticpytools/environment/core.py,sha256=t2pGPrj3QGt3MK4PbHSm6lKFkkZd7wJQz91X7h6PtPw,704
|
|
3
|
+
agent_repl_mcp/agenticpytools/kernel/__init__.py,sha256=po__3LcIPgMV32B8-iWODSE7cNcQ3n7QhJqH9YY8zGk,152
|
|
4
|
+
agent_repl_mcp/agenticpytools/kernel/client.py,sha256=8LGFBufzqt5aGfXBxGiG5PSiOM476vh2SyumV1xygQo,1722
|
|
5
|
+
agent_repl_mcp/agenticpytools/kernel/close.py,sha256=ppfCCijEwD67KjuCo6ZZ-jcFwLWV_nQ3bbIvwyoNKA4,788
|
|
6
|
+
agent_repl_mcp/agenticpytools/kernel/content.py,sha256=sObkQN9ghF3e03aHvbzgnNc17au3U-8SOzXUP0ADPZQ,1105
|
|
7
|
+
agent_repl_mcp/agenticpytools/kernel/core.py,sha256=DaeCPn17OLbQC-Unce21TjxNLSdANsQcT_GlOvvadj8,603
|
|
8
|
+
agent_repl_mcp/agenticpytools/kernel/create.py,sha256=_TEW-cnqodv6fwqLt1Vo-9T7QyHSiIg8JLcPIGeEbvM,1711
|
|
9
|
+
agent_repl_mcp/agenticpytools/repl_mcp/__init__.py,sha256=ux7CpYqA3PrGx4wgYp4E60tk7YJ1ufo3l7PDNW7HXMg,74
|
|
10
|
+
agent_repl_mcp/agenticpytools/repl_mcp/core.py,sha256=GdVHiSYx0UbiaFDK-4eBKnWfKClOqCHIJ5QEQ3z7jJE,1231
|
|
11
|
+
agent_repl_mcp/agenticpytools/storage/__init__.py,sha256=kJyk4eKKx5BkGnOYy9C2Ll9bmftde0vIzZCVaVrHvHA,176
|
|
12
|
+
agent_repl_mcp/agenticpytools/storage/connection.py,sha256=aCZXAX7AXiAVI3I7ggITkemHs9NAX6CpqVt4rJqxLd8,584
|
|
13
|
+
agent_repl_mcp/agenticpytools/storage/core.py,sha256=ErKaP5_xmY0L3tO31AW_OZi2hXcWxfSldWX4Vf3yuog,1285
|
|
14
|
+
agent_repl_mcp/agenticpytools/storage/schema.py,sha256=ghEjkhiNOM0nH9oXZYnJ-NVe8LH9jWCQ2MJg2PzZM0E,301
|
|
15
|
+
agent_repl_mcp-0.0.1.dist-info/METADATA,sha256=D-K9n7RaLhLVLqQp-SCe-9yte9qi8o6SAx96ZAyu8aM,319
|
|
16
|
+
agent_repl_mcp-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
17
|
+
agent_repl_mcp-0.0.1.dist-info/RECORD,,
|