loop-mcp 0.1.0__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,56 @@
1
+ # Secrets (never commit)
2
+ .pypirc
3
+ .npmrc
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ venv/
27
+ env/
28
+ ENV/
29
+
30
+ # Node
31
+ node_modules/
32
+ npm-debug.log*
33
+ yarn-debug.log*
34
+ yarn-error.log*
35
+ dist/
36
+ build/
37
+ *.tsbuildinfo
38
+
39
+ # IDE
40
+ .vscode/
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+ .DS_Store
46
+
47
+ # Loop state (local only)
48
+ .loop/
49
+
50
+ # Testing
51
+ .pytest_cache/
52
+ coverage/
53
+ .nyc_output/
54
+ *.log
55
+
56
+ \IMPLEMENTATION_COMPLETE.md
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: loop-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for automated loop engineering in AI coding workflows
5
+ Project-URL: Homepage, https://github.com/yourusername/loop-engineering
6
+ Project-URL: Documentation, https://github.com/yourusername/loop-engineering#readme
7
+ Project-URL: Repository, https://github.com/yourusername/loop-engineering
8
+ Project-URL: Issues, https://github.com/yourusername/loop-engineering/issues
9
+ Author: Loop Engineering Contributors
10
+ License-Expression: MIT
11
+ Keywords: ai,automation,coding,loop-engineering,mcp
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: aiofiles>=23.0.0
21
+ Requires-Dist: croniter>=2.0.0
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: mcp>=0.9.0
24
+ Requires-Dist: pydantic>=2.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: black>=23.0.0; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Loop Engineering MCP Server (Python)
33
+
34
+ Python implementation of the Loop Engineering MCP server.
35
+
36
+ ## Installation
37
+
38
+ ### Using uvx (recommended - no installation needed)
39
+
40
+ ```bash
41
+ uvx loop-mcp
42
+ ```
43
+
44
+ ### Using pip
45
+
46
+ ```bash
47
+ pip install loop-mcp
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ Add to your MCP configuration file:
53
+
54
+ **Cursor** (`.cursor/mcp.json`):
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "loop-engineering": {
59
+ "command": "uvx",
60
+ "args": ["loop-mcp"]
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ **Kiro** (`.kiro/settings/mcp.json`):
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "loop-engineering": {
71
+ "command": "uvx",
72
+ "args": ["loop-mcp"]
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ **Claude Desktop**:
79
+ - Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
80
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "loop-engineering": {
86
+ "command": "uvx",
87
+ "args": ["loop-mcp"]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ cd python
97
+ pip install -e ".[dev]"
98
+ pytest
99
+ ```
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,72 @@
1
+ # Loop Engineering MCP Server (Python)
2
+
3
+ Python implementation of the Loop Engineering MCP server.
4
+
5
+ ## Installation
6
+
7
+ ### Using uvx (recommended - no installation needed)
8
+
9
+ ```bash
10
+ uvx loop-mcp
11
+ ```
12
+
13
+ ### Using pip
14
+
15
+ ```bash
16
+ pip install loop-mcp
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Add to your MCP configuration file:
22
+
23
+ **Cursor** (`.cursor/mcp.json`):
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "loop-engineering": {
28
+ "command": "uvx",
29
+ "args": ["loop-mcp"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ **Kiro** (`.kiro/settings/mcp.json`):
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "loop-engineering": {
40
+ "command": "uvx",
41
+ "args": ["loop-mcp"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ **Claude Desktop**:
48
+ - Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
49
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "loop-engineering": {
55
+ "command": "uvx",
56
+ "args": ["loop-mcp"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## Development
63
+
64
+ ```bash
65
+ cd python
66
+ pip install -e ".[dev]"
67
+ pytest
68
+ ```
69
+
70
+ ## License
71
+
72
+ MIT
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "loop-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for automated loop engineering in AI coding workflows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ keywords = ["mcp", "ai", "coding", "automation", "loop-engineering"]
13
+ authors = [
14
+ { name = "Loop Engineering Contributors" }
15
+ ]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ ]
25
+ dependencies = [
26
+ "mcp>=0.9.0",
27
+ "pydantic>=2.0.0",
28
+ "aiofiles>=23.0.0",
29
+ "httpx>=0.27.0",
30
+ "croniter>=2.0.0",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0.0",
36
+ "pytest-asyncio>=0.21.0",
37
+ "black>=23.0.0",
38
+ "ruff>=0.1.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/yourusername/loop-engineering"
43
+ Documentation = "https://github.com/yourusername/loop-engineering#readme"
44
+ Repository = "https://github.com/yourusername/loop-engineering"
45
+ Issues = "https://github.com/yourusername/loop-engineering/issues"
46
+
47
+ [project.scripts]
48
+ loop-mcp = "loop_engineering_mcp.__main__:main"
49
+ loop-mcp-worker = "loop_engineering_mcp.worker:main"
50
+
51
+ [tool.pytest.ini_options]
52
+ asyncio_mode = "auto"
53
+ testpaths = ["tests"]
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/loop_engineering_mcp"]
57
+
58
+ [tool.black]
59
+ line-length = 100
60
+ target-version = ["py310"]
61
+
62
+ [tool.ruff]
63
+ line-length = 100
64
+ target-version = "py310"
@@ -0,0 +1,7 @@
1
+ """Loop Engineering MCP Server - Python Implementation."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .server import create_server
6
+
7
+ __all__ = ["create_server", "__version__"]
@@ -0,0 +1,37 @@
1
+ """Entry point for the Loop Engineering MCP server."""
2
+
3
+ import asyncio
4
+ import sys
5
+ from mcp.server.stdio import stdio_server
6
+ from .server import create_server
7
+
8
+
9
+ async def _run_server():
10
+ """Run the MCP server with stdio transport."""
11
+ async with stdio_server() as (read_stream, write_stream):
12
+ server = create_server(start_scheduler=True)
13
+ scheduler = getattr(server, "_loop_scheduler", None)
14
+ if scheduler:
15
+ scheduler.start()
16
+ try:
17
+ init_options = server.create_initialization_options()
18
+ await server.run(read_stream, write_stream, init_options)
19
+ finally:
20
+ if scheduler:
21
+ await scheduler.stop()
22
+
23
+
24
+ def main():
25
+ """Run the MCP server."""
26
+ try:
27
+ asyncio.run(_run_server())
28
+ except KeyboardInterrupt:
29
+ print("\nShutting down Loop Engineering MCP server...", file=sys.stderr)
30
+ sys.exit(0)
31
+ except Exception as e:
32
+ print(f"Error running server: {e}", file=sys.stderr)
33
+ sys.exit(1)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,130 @@
1
+ """GitHub API integration for PR creation."""
2
+
3
+ import os
4
+ import subprocess
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ import httpx
9
+
10
+ from .retry import retry_async
11
+
12
+
13
+ @dataclass
14
+ class PullRequestResult:
15
+ """Result of creating a pull request."""
16
+
17
+ success: bool
18
+ pr_number: Optional[int] = None
19
+ pr_url: Optional[str] = None
20
+ branch: Optional[str] = None
21
+ error: Optional[str] = None
22
+
23
+
24
+ class GitHubClient:
25
+ """Creates branches and pull requests via the GitHub REST API."""
26
+
27
+ API_BASE = "https://api.github.com"
28
+
29
+ def __init__(
30
+ self,
31
+ token: Optional[str] = None,
32
+ repo: Optional[str] = None,
33
+ default_branch: str = "main",
34
+ ):
35
+ self.token = token or os.environ.get("GITHUB_TOKEN")
36
+ self.repo = repo or os.environ.get("GITHUB_REPO") or self._detect_repo()
37
+ self.default_branch = default_branch or os.environ.get("GITHUB_DEFAULT_BRANCH", "main")
38
+
39
+ def _detect_repo(self) -> Optional[str]:
40
+ try:
41
+ result = subprocess.run(
42
+ ["git", "remote", "get-url", "origin"],
43
+ capture_output=True,
44
+ text=True,
45
+ timeout=10,
46
+ )
47
+ if result.returncode != 0:
48
+ return None
49
+ url = result.stdout.strip()
50
+ # Handle git@github.com:owner/repo.git and https://github.com/owner/repo.git
51
+ if "github.com" in url:
52
+ parts = url.rstrip(".git").split("/")
53
+ if len(parts) >= 2:
54
+ return f"{parts[-2]}/{parts[-1]}"
55
+ return None
56
+ except Exception:
57
+ return None
58
+
59
+ def _headers(self) -> dict[str, str]:
60
+ if not self.token:
61
+ raise ValueError("GITHUB_TOKEN environment variable is required")
62
+ return {
63
+ "Authorization": f"Bearer {self.token}",
64
+ "Accept": "application/vnd.github+json",
65
+ "X-GitHub-Api-Version": "2022-11-28",
66
+ }
67
+
68
+ async def create_pull_request(
69
+ self,
70
+ *,
71
+ title: str,
72
+ body: str,
73
+ branch: str,
74
+ base: Optional[str] = None,
75
+ ) -> PullRequestResult:
76
+ """Create a pull request on GitHub."""
77
+ if not self.repo:
78
+ return PullRequestResult(
79
+ success=False,
80
+ error="Could not detect GitHub repo. Set GITHUB_REPO=owner/repo",
81
+ )
82
+
83
+ base_branch = base or self.default_branch
84
+
85
+ async def _create() -> PullRequestResult:
86
+ async with httpx.AsyncClient(timeout=30.0) as client:
87
+ response = await client.post(
88
+ f"{self.API_BASE}/repos/{self.repo}/pulls",
89
+ headers=self._headers(),
90
+ json={
91
+ "title": title,
92
+ "body": body,
93
+ "head": branch,
94
+ "base": base_branch,
95
+ },
96
+ )
97
+ if response.status_code == 422:
98
+ data = response.json()
99
+ errors = data.get("errors", [])
100
+ msg = errors[0].get("message") if errors else data.get("message", "Validation failed")
101
+ return PullRequestResult(success=False, error=msg)
102
+ response.raise_for_status()
103
+ data = response.json()
104
+ return PullRequestResult(
105
+ success=True,
106
+ pr_number=data["number"],
107
+ pr_url=data["html_url"],
108
+ branch=branch,
109
+ )
110
+
111
+ try:
112
+ return await retry_async(_create, max_attempts=3)
113
+ except Exception as e:
114
+ return PullRequestResult(success=False, error=str(e))
115
+
116
+ async def push_branch(self, branch: str, cwd: Optional[str] = None) -> tuple[bool, str]:
117
+ """Push the current branch to origin."""
118
+ try:
119
+ result = subprocess.run(
120
+ ["git", "push", "-u", "origin", branch],
121
+ capture_output=True,
122
+ text=True,
123
+ timeout=120,
124
+ cwd=cwd,
125
+ )
126
+ if result.returncode != 0:
127
+ return False, result.stderr or result.stdout
128
+ return True, "Branch pushed successfully"
129
+ except Exception as e:
130
+ return False, str(e)
@@ -0,0 +1,60 @@
1
+ """Structured logging for loop engineering."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+
10
+ class LoopLogger:
11
+ """Writes structured JSON logs to .loop/logs/."""
12
+
13
+ def __init__(self, logs_dir: Path):
14
+ self.logs_dir = logs_dir
15
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
16
+ self._logger = logging.getLogger("loop_engineering")
17
+ if not self._logger.handlers:
18
+ handler = logging.StreamHandler()
19
+ handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
20
+ self._logger.addHandler(handler)
21
+ self._logger.setLevel(logging.INFO)
22
+
23
+ def _write_json(self, filename: str, entry: dict[str, Any]) -> None:
24
+ log_file = self.logs_dir / filename
25
+ with open(log_file, "a", encoding="utf-8") as f:
26
+ f.write(json.dumps(entry, default=str) + "\n")
27
+
28
+ def _entry(
29
+ self,
30
+ level: str,
31
+ event: str,
32
+ loop_name: Optional[str] = None,
33
+ **extra: Any,
34
+ ) -> dict[str, Any]:
35
+ return {
36
+ "timestamp": datetime.now(timezone.utc).isoformat(),
37
+ "level": level,
38
+ "event": event,
39
+ "loop_name": loop_name,
40
+ **extra,
41
+ }
42
+
43
+ def info(self, event: str, loop_name: Optional[str] = None, **extra: Any) -> None:
44
+ entry = self._entry("INFO", event, loop_name, **extra)
45
+ self._write_json("loop-engineering.log", entry)
46
+ self._logger.info(f"[{loop_name or 'system'}] {event}")
47
+
48
+ def warning(self, event: str, loop_name: Optional[str] = None, **extra: Any) -> None:
49
+ entry = self._entry("WARNING", event, loop_name, **extra)
50
+ self._write_json("loop-engineering.log", entry)
51
+ self._logger.warning(f"[{loop_name or 'system'}] {event}")
52
+
53
+ def error(self, event: str, loop_name: Optional[str] = None, **extra: Any) -> None:
54
+ entry = self._entry("ERROR", event, loop_name, **extra)
55
+ self._write_json("loop-engineering.log", entry)
56
+ self._logger.error(f"[{loop_name or 'system'}] {event}")
57
+
58
+ def run_log(self, loop_name: str, run_id: str, data: dict[str, Any]) -> None:
59
+ entry = self._entry("INFO", "loop_run", loop_name, run_id=run_id, **data)
60
+ self._write_json(f"{loop_name}-runs.log", entry)