loopengt 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.
- loopengt/__init__.py +31 -0
- loopengt/adapters/__init__.py +1 -0
- loopengt/adapters/antigravity/__init__.py +1 -0
- loopengt/adapters/antigravity/adapter.py +55 -0
- loopengt/adapters/antigravity/commands.py +21 -0
- loopengt/adapters/base.py +51 -0
- loopengt/adapters/claude_code/__init__.py +1 -0
- loopengt/adapters/claude_code/adapter.py +55 -0
- loopengt/adapters/claude_code/commands.py +16 -0
- loopengt/adapters/codex/__init__.py +1 -0
- loopengt/adapters/codex/adapter.py +52 -0
- loopengt/adapters/codex/commands.py +16 -0
- loopengt/adapters/cursor/__init__.py +1 -0
- loopengt/adapters/cursor/adapter.py +56 -0
- loopengt/adapters/cursor/commands.py +29 -0
- loopengt/adapters/generic/__init__.py +1 -0
- loopengt/adapters/generic/terminal.py +82 -0
- loopengt/cli/__init__.py +1 -0
- loopengt/cli/commands/__init__.py +1 -0
- loopengt/cli/commands/design.py +171 -0
- loopengt/cli/commands/doctor.py +110 -0
- loopengt/cli/commands/eval.py +105 -0
- loopengt/cli/commands/init.py +131 -0
- loopengt/cli/commands/mcp_serve.py +57 -0
- loopengt/cli/commands/run.py +99 -0
- loopengt/cli/commands/template.py +145 -0
- loopengt/cli/commands/trace.py +114 -0
- loopengt/cli/formatters.py +125 -0
- loopengt/cli/main.py +66 -0
- loopengt/core/__init__.py +1 -0
- loopengt/core/evals/__init__.py +1 -0
- loopengt/core/evals/judges.py +216 -0
- loopengt/core/evals/metrics.py +119 -0
- loopengt/core/evals/regression.py +157 -0
- loopengt/core/memory/__init__.py +1 -0
- loopengt/core/memory/retrieval.py +124 -0
- loopengt/core/memory/store.py +184 -0
- loopengt/core/memory/summarizer.py +97 -0
- loopengt/core/models/__init__.py +43 -0
- loopengt/core/models/agent.py +126 -0
- loopengt/core/models/loop_spec.py +251 -0
- loopengt/core/models/policy.py +131 -0
- loopengt/core/models/state.py +271 -0
- loopengt/core/models/tool.py +105 -0
- loopengt/core/runtime/__init__.py +1 -0
- loopengt/core/runtime/checkpoint.py +152 -0
- loopengt/core/runtime/executor.py +463 -0
- loopengt/core/runtime/handoff.py +139 -0
- loopengt/core/runtime/scheduler.py +168 -0
- loopengt/core/tracing/__init__.py +1 -0
- loopengt/core/tracing/events.py +95 -0
- loopengt/core/tracing/exporters.py +158 -0
- loopengt/core/tracing/store.py +202 -0
- loopengt/mcp/__init__.py +1 -0
- loopengt/mcp/client/__init__.py +1 -0
- loopengt/mcp/client/manager.py +118 -0
- loopengt/mcp/client/tools.py +107 -0
- loopengt/mcp/server/__init__.py +1 -0
- loopengt/mcp/server/prompts.py +82 -0
- loopengt/mcp/server/resources.py +75 -0
- loopengt/mcp/server/server.py +50 -0
- loopengt/mcp/server/tools.py +214 -0
- loopengt/mcp/shared/__init__.py +1 -0
- loopengt/mcp/shared/schemas.py +91 -0
- loopengt/plugins/__init__.py +1 -0
- loopengt/plugins/base.py +90 -0
- loopengt/plugins/loader.py +130 -0
- loopengt/plugins/manifest.py +70 -0
- loopengt/plugins/registry.py +146 -0
- loopengt/prompts/LOOPENGT.md +60 -0
- loopengt/prompts/__init__.py +1 -0
- loopengt/storage/__init__.py +1 -0
- loopengt/storage/jsonl.py +84 -0
- loopengt/storage/sqlite.py +102 -0
- loopengt/templates/__init__.py +1 -0
- loopengt/templates/builtins/handoff_loop/LOOPENGS.md +10 -0
- loopengt/templates/builtins/planner_executor/LOOPENGS.md +29 -0
- loopengt/templates/builtins/research_architect/LOOPENGS.md +17 -0
- loopengt/templates/builtins/reviewer_retry/LOOPENGS.md +29 -0
- loopengt/templates/builtins/supervisor_workers/LOOPENGS.md +29 -0
- loopengt/templates/loader.py +38 -0
- loopengt/templates/registry.py +85 -0
- loopengt-0.1.0.dist-info/METADATA +275 -0
- loopengt-0.1.0.dist-info/RECORD +87 -0
- loopengt-0.1.0.dist-info/WHEEL +4 -0
- loopengt-0.1.0.dist-info/entry_points.txt +8 -0
- loopengt-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""MCP client connection management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
logger = structlog.get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MCPClientManager:
|
|
13
|
+
"""Manages connections to external MCP servers.
|
|
14
|
+
|
|
15
|
+
Handles server discovery, connection lifecycle, and reconnection.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
manager = MCPClientManager()
|
|
20
|
+
await manager.connect("github-server", transport="stdio", command=["mcp-github"])
|
|
21
|
+
tools = await manager.list_tools("github-server")
|
|
22
|
+
result = await manager.call_tool("github-server", "search_repos", {"query": "loopengt"})
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._connections: dict[str, Any] = {}
|
|
27
|
+
self._log = logger.bind(component="mcp_client")
|
|
28
|
+
|
|
29
|
+
async def connect(
|
|
30
|
+
self,
|
|
31
|
+
server_name: str,
|
|
32
|
+
*,
|
|
33
|
+
transport: str = "stdio",
|
|
34
|
+
command: list[str] | None = None,
|
|
35
|
+
url: str | None = None,
|
|
36
|
+
env: dict[str, str] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Connect to an MCP server.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
server_name:
|
|
43
|
+
Unique identifier for this connection.
|
|
44
|
+
transport:
|
|
45
|
+
"stdio" or "sse".
|
|
46
|
+
command:
|
|
47
|
+
Command to launch (stdio transport).
|
|
48
|
+
url:
|
|
49
|
+
Server URL (SSE transport).
|
|
50
|
+
env:
|
|
51
|
+
Environment variables to pass.
|
|
52
|
+
"""
|
|
53
|
+
self._log.info(
|
|
54
|
+
"mcp_client.connect",
|
|
55
|
+
server=server_name,
|
|
56
|
+
transport=transport,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Store connection config (actual MCP client integration deferred)
|
|
60
|
+
self._connections[server_name] = {
|
|
61
|
+
"transport": transport,
|
|
62
|
+
"command": command,
|
|
63
|
+
"url": url,
|
|
64
|
+
"env": env or {},
|
|
65
|
+
"connected": True,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async def disconnect(self, server_name: str) -> None:
|
|
69
|
+
"""Disconnect from an MCP server."""
|
|
70
|
+
if server_name in self._connections:
|
|
71
|
+
del self._connections[server_name]
|
|
72
|
+
self._log.info("mcp_client.disconnect", server=server_name)
|
|
73
|
+
|
|
74
|
+
async def disconnect_all(self) -> None:
|
|
75
|
+
"""Disconnect from all MCP servers."""
|
|
76
|
+
self._connections.clear()
|
|
77
|
+
|
|
78
|
+
async def list_tools(self, server_name: str) -> list[dict[str, Any]]:
|
|
79
|
+
"""List tools available on a connected MCP server."""
|
|
80
|
+
if server_name not in self._connections:
|
|
81
|
+
raise ConnectionError(f"Not connected to server: {server_name}")
|
|
82
|
+
|
|
83
|
+
# Stub — returns empty until real MCP client is wired
|
|
84
|
+
self._log.info("mcp_client.list_tools", server=server_name)
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
async def call_tool(
|
|
88
|
+
self,
|
|
89
|
+
server_name: str,
|
|
90
|
+
tool_name: str,
|
|
91
|
+
arguments: dict[str, Any] | None = None,
|
|
92
|
+
) -> Any:
|
|
93
|
+
"""Call a tool on a connected MCP server."""
|
|
94
|
+
if server_name not in self._connections:
|
|
95
|
+
raise ConnectionError(f"Not connected to server: {server_name}")
|
|
96
|
+
|
|
97
|
+
self._log.info(
|
|
98
|
+
"mcp_client.call_tool",
|
|
99
|
+
server=server_name,
|
|
100
|
+
tool=tool_name,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Stub response
|
|
104
|
+
return {
|
|
105
|
+
"server": server_name,
|
|
106
|
+
"tool": tool_name,
|
|
107
|
+
"arguments": arguments,
|
|
108
|
+
"result": f"[stub] {tool_name} called on {server_name}",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def connected_servers(self) -> list[str]:
|
|
113
|
+
"""List names of connected servers."""
|
|
114
|
+
return list(self._connections.keys())
|
|
115
|
+
|
|
116
|
+
def is_connected(self, server_name: str) -> bool:
|
|
117
|
+
"""Check if a server is connected."""
|
|
118
|
+
return server_name in self._connections
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""MCP tool invocation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
from loopengt.mcp.client.manager import MCPClientManager
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPToolInvoker:
|
|
15
|
+
"""High-level tool invocation over MCP.
|
|
16
|
+
|
|
17
|
+
Wraps ``MCPClientManager`` with retry, timeout, and error handling.
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
invoker = MCPToolInvoker(manager)
|
|
22
|
+
result = await invoker.invoke("server", "tool_name", {"arg": "value"})
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
manager: MCPClientManager,
|
|
28
|
+
*,
|
|
29
|
+
default_timeout: float = 30.0,
|
|
30
|
+
max_retries: int = 2,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._manager = manager
|
|
33
|
+
self._timeout = default_timeout
|
|
34
|
+
self._max_retries = max_retries
|
|
35
|
+
self._log = logger.bind(component="mcp_invoker")
|
|
36
|
+
|
|
37
|
+
async def invoke(
|
|
38
|
+
self,
|
|
39
|
+
server_name: str,
|
|
40
|
+
tool_name: str,
|
|
41
|
+
arguments: dict[str, Any] | None = None,
|
|
42
|
+
*,
|
|
43
|
+
timeout: float | None = None,
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""Invoke an MCP tool with retry logic.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
server_name:
|
|
50
|
+
Name of the MCP server.
|
|
51
|
+
tool_name:
|
|
52
|
+
Name of the tool to invoke.
|
|
53
|
+
arguments:
|
|
54
|
+
Tool arguments.
|
|
55
|
+
timeout:
|
|
56
|
+
Per-call timeout override.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
Any
|
|
61
|
+
The tool's result payload.
|
|
62
|
+
"""
|
|
63
|
+
effective_timeout = timeout or self._timeout
|
|
64
|
+
|
|
65
|
+
for attempt in range(self._max_retries + 1):
|
|
66
|
+
try:
|
|
67
|
+
self._log.info(
|
|
68
|
+
"mcp.invoke",
|
|
69
|
+
server=server_name,
|
|
70
|
+
tool=tool_name,
|
|
71
|
+
attempt=attempt + 1,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
result = await self._manager.call_tool(
|
|
75
|
+
server_name, tool_name, arguments
|
|
76
|
+
)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
except Exception as exc: # noqa: BLE001
|
|
80
|
+
self._log.warning(
|
|
81
|
+
"mcp.invoke_error",
|
|
82
|
+
server=server_name,
|
|
83
|
+
tool=tool_name,
|
|
84
|
+
attempt=attempt + 1,
|
|
85
|
+
error=str(exc),
|
|
86
|
+
)
|
|
87
|
+
if attempt >= self._max_retries:
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
return None # unreachable but satisfies type checker
|
|
91
|
+
|
|
92
|
+
async def invoke_many(
|
|
93
|
+
self,
|
|
94
|
+
calls: list[tuple[str, str, dict[str, Any] | None]],
|
|
95
|
+
) -> list[Any]:
|
|
96
|
+
"""Invoke multiple tools sequentially.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
calls:
|
|
101
|
+
List of (server_name, tool_name, arguments) tuples.
|
|
102
|
+
"""
|
|
103
|
+
results = []
|
|
104
|
+
for server, tool, args in calls:
|
|
105
|
+
result = await self.invoke(server, tool, args)
|
|
106
|
+
results.append(result)
|
|
107
|
+
return results
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP server subpackage."""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""MCP server prompts — loop engineering prompts exposed via MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
logger = structlog.get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register_prompts(server: Any) -> None:
|
|
13
|
+
"""Register loopengt prompts with the FastMCP server."""
|
|
14
|
+
|
|
15
|
+
@server.prompt()
|
|
16
|
+
async def design(goal: str) -> str:
|
|
17
|
+
"""Design a new agent loop from a natural-language goal.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
goal: The objective for the loop to achieve.
|
|
21
|
+
"""
|
|
22
|
+
return (
|
|
23
|
+
"You are the Loop Engineering Architect (LOOPENGT).\n\n"
|
|
24
|
+
f"## Goal\n{goal}\n\n"
|
|
25
|
+
"## Instructions\n"
|
|
26
|
+
"Design an optimal agent loop specification for the above goal.\n\n"
|
|
27
|
+
"1. Select the best orchestration pattern (sequential, supervisor_worker, "
|
|
28
|
+
"parallel_fan_out, handoff, evaluator_optimizer)\n"
|
|
29
|
+
"2. Define the minimum necessary agents with clear roles\n"
|
|
30
|
+
"3. Specify the execution steps with dependencies\n"
|
|
31
|
+
"4. Add verification gates at critical boundaries\n"
|
|
32
|
+
"5. Define stop conditions and policies\n\n"
|
|
33
|
+
"Output a complete `loop.yaml` specification and a `LOOP_DESIGN.md` "
|
|
34
|
+
"design document."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@server.prompt()
|
|
38
|
+
async def review(loop_yaml: str) -> str:
|
|
39
|
+
"""Review an existing loop specification for correctness and efficiency.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
loop_yaml: The YAML content of the loop specification.
|
|
43
|
+
"""
|
|
44
|
+
return (
|
|
45
|
+
"You are a Loop Engineering Reviewer.\n\n"
|
|
46
|
+
"## Loop Specification\n"
|
|
47
|
+
f"```yaml\n{loop_yaml}\n```\n\n"
|
|
48
|
+
"## Review Criteria\n"
|
|
49
|
+
"1. **Correctness**: Are step dependencies valid? Do agents exist?\n"
|
|
50
|
+
"2. **Efficiency**: Are there redundant steps or agents?\n"
|
|
51
|
+
"3. **Resilience**: Are retry policies and verification gates adequate?\n"
|
|
52
|
+
"4. **Observability**: Are traces and logging sufficient?\n"
|
|
53
|
+
"5. **Security**: Are tool auth configs appropriate?\n\n"
|
|
54
|
+
"Provide specific, actionable feedback."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@server.prompt()
|
|
58
|
+
async def improve(loop_yaml: str, feedback: str = "") -> str:
|
|
59
|
+
"""Improve a loop specification based on feedback or best practices.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
loop_yaml: The current YAML specification.
|
|
63
|
+
feedback: Optional specific feedback to address.
|
|
64
|
+
"""
|
|
65
|
+
feedback_section = (
|
|
66
|
+
f"\n## Feedback to Address\n{feedback}\n" if feedback else ""
|
|
67
|
+
)
|
|
68
|
+
return (
|
|
69
|
+
"You are a Loop Engineering Optimizer.\n\n"
|
|
70
|
+
"## Current Specification\n"
|
|
71
|
+
f"```yaml\n{loop_yaml}\n```\n"
|
|
72
|
+
f"{feedback_section}\n"
|
|
73
|
+
"## Optimization Goals\n"
|
|
74
|
+
"1. Reduce unnecessary complexity\n"
|
|
75
|
+
"2. Improve error handling and recovery\n"
|
|
76
|
+
"3. Add missing verification gates\n"
|
|
77
|
+
"4. Optimize agent capabilities and tool assignments\n"
|
|
78
|
+
"5. Enhance observability\n\n"
|
|
79
|
+
"Output an improved `loop.yaml` with explanations for each change."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
logger.info("mcp.prompts.registered", count=3)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""MCP server resources — loop specs, traces, and templates as resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
logger = structlog.get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_resources(server: Any) -> None:
|
|
16
|
+
"""Register loopengt resources with the FastMCP server."""
|
|
17
|
+
|
|
18
|
+
@server.resource("loop://spec/{name}")
|
|
19
|
+
async def get_loop_spec(name: str) -> str:
|
|
20
|
+
"""Retrieve a loop specification by name.
|
|
21
|
+
|
|
22
|
+
Searches .loopengt/templates/ and the current directory.
|
|
23
|
+
"""
|
|
24
|
+
search_paths = [
|
|
25
|
+
Path(f".loopengt/templates/{name}/loop.yaml"),
|
|
26
|
+
Path(f"{name}.yaml"),
|
|
27
|
+
Path(f"{name}/loop.yaml"),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
for path in search_paths:
|
|
31
|
+
if path.exists():
|
|
32
|
+
content = path.read_text(encoding="utf-8")
|
|
33
|
+
return content
|
|
34
|
+
|
|
35
|
+
return json.dumps({"error": f"Loop spec '{name}' not found"})
|
|
36
|
+
|
|
37
|
+
@server.resource("trace://run/{run_id}")
|
|
38
|
+
async def get_run_trace(run_id: str) -> str:
|
|
39
|
+
"""Retrieve the execution trace for a run."""
|
|
40
|
+
trace_path = Path(f".loopengt/runs/{run_id}.json")
|
|
41
|
+
if trace_path.exists():
|
|
42
|
+
return trace_path.read_text(encoding="utf-8")
|
|
43
|
+
return json.dumps({"error": f"Trace '{run_id}' not found"})
|
|
44
|
+
|
|
45
|
+
@server.resource("template://list")
|
|
46
|
+
async def list_templates() -> str:
|
|
47
|
+
"""List all available loop templates."""
|
|
48
|
+
templates = []
|
|
49
|
+
|
|
50
|
+
# Built-in templates
|
|
51
|
+
builtins = {
|
|
52
|
+
"planner_executor": "Sequential plan-then-execute pattern",
|
|
53
|
+
"reviewer_retry": "Code review with iterative retry on failure",
|
|
54
|
+
"supervisor_workers": "Supervisor delegates to specialised workers",
|
|
55
|
+
"research_architect": "Research → synthesise → architect workflow",
|
|
56
|
+
"handoff_loop": "Sequential agent-to-agent handoff pattern",
|
|
57
|
+
}
|
|
58
|
+
for name, desc in builtins.items():
|
|
59
|
+
templates.append({"name": name, "source": "builtin", "description": desc})
|
|
60
|
+
|
|
61
|
+
# Local templates
|
|
62
|
+
local_dir = Path(".loopengt/templates")
|
|
63
|
+
if local_dir.exists():
|
|
64
|
+
for entry in sorted(local_dir.iterdir()):
|
|
65
|
+
if entry.is_dir() and (entry / "loop.yaml").exists():
|
|
66
|
+
if entry.name not in builtins:
|
|
67
|
+
templates.append({
|
|
68
|
+
"name": entry.name,
|
|
69
|
+
"source": "local",
|
|
70
|
+
"description": "",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return json.dumps(templates, indent=2)
|
|
74
|
+
|
|
75
|
+
logger.info("mcp.resources.registered", count=3)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""FastMCP server setup — the loopengt MCP server entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
logger = structlog.get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_server() -> Any:
|
|
13
|
+
"""Create and configure the loopengt FastMCP server.
|
|
14
|
+
|
|
15
|
+
Returns a FastMCP server instance with all tools, resources, and
|
|
16
|
+
prompts registered.
|
|
17
|
+
|
|
18
|
+
Raises
|
|
19
|
+
------
|
|
20
|
+
ImportError
|
|
21
|
+
If fastmcp is not installed.
|
|
22
|
+
"""
|
|
23
|
+
from fastmcp import FastMCP
|
|
24
|
+
|
|
25
|
+
server = FastMCP(
|
|
26
|
+
name="loopengt",
|
|
27
|
+
instructions=(
|
|
28
|
+
"Loop Engineering Agent — design, orchestrate, and evaluate agent loops. "
|
|
29
|
+
"Use the provided tools to create loop specifications, execute loops, "
|
|
30
|
+
"inspect traces, and manage templates."
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Register tools
|
|
35
|
+
from loopengt.mcp.server.tools import register_tools
|
|
36
|
+
|
|
37
|
+
register_tools(server)
|
|
38
|
+
|
|
39
|
+
# Register resources
|
|
40
|
+
from loopengt.mcp.server.resources import register_resources
|
|
41
|
+
|
|
42
|
+
register_resources(server)
|
|
43
|
+
|
|
44
|
+
# Register prompts
|
|
45
|
+
from loopengt.mcp.server.prompts import register_prompts
|
|
46
|
+
|
|
47
|
+
register_prompts(server)
|
|
48
|
+
|
|
49
|
+
logger.info("mcp.server.created")
|
|
50
|
+
return server
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""MCP server tools — loop lifecycle operations exposed over MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
logger = structlog.get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_tools(server: Any) -> None:
|
|
17
|
+
"""Register loopengt tools with the FastMCP server."""
|
|
18
|
+
|
|
19
|
+
@server.tool()
|
|
20
|
+
async def create_loop(
|
|
21
|
+
name: str,
|
|
22
|
+
goal: str,
|
|
23
|
+
pattern: str = "sequential",
|
|
24
|
+
agents: list[dict[str, Any]] | None = None,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
"""Create a new loop specification.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
name: Loop identifier.
|
|
30
|
+
goal: The objective this loop achieves.
|
|
31
|
+
pattern: Orchestration pattern (sequential, supervisor_worker, etc.).
|
|
32
|
+
agents: Optional list of agent definitions.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The created loop specification as a dict.
|
|
36
|
+
"""
|
|
37
|
+
from loopengt.cli.commands.design import _generate_stub_spec, _generate_stub_design
|
|
38
|
+
import yaml
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
spec = _generate_stub_spec(goal, template=None)
|
|
42
|
+
spec["name"] = name
|
|
43
|
+
spec["pattern"] = pattern
|
|
44
|
+
|
|
45
|
+
if agents:
|
|
46
|
+
spec["agents"] = agents
|
|
47
|
+
|
|
48
|
+
design_md = _generate_stub_design(goal, spec)
|
|
49
|
+
|
|
50
|
+
# Write files to disk
|
|
51
|
+
Path("loop.yaml").write_text(yaml.dump(spec, sort_keys=False), encoding="utf-8")
|
|
52
|
+
Path("loopengt.md").write_text(design_md, encoding="utf-8")
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"status": "created",
|
|
56
|
+
"spec": spec,
|
|
57
|
+
"message": "Loop architecture designed and saved to loopengt.md and loop.yaml. Please read loopengt.md and act as the orchestrator: execute the steps defined by invoking your coding capabilities to fulfill the roles of each agent described in the document."
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@server.tool()
|
|
61
|
+
async def run_loop(
|
|
62
|
+
spec: dict[str, Any] | None = None,
|
|
63
|
+
spec_path: str | None = None,
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Execute a loop from a spec dict or YAML file path.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
spec: Loop specification as a dict.
|
|
69
|
+
spec_path: Path to a loop.yaml file.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Execution result with run_id and status.
|
|
73
|
+
"""
|
|
74
|
+
if spec is None and spec_path:
|
|
75
|
+
path = Path(spec_path)
|
|
76
|
+
if not path.exists():
|
|
77
|
+
return {"error": f"Spec file not found: {spec_path}"}
|
|
78
|
+
spec = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
79
|
+
|
|
80
|
+
if spec is None:
|
|
81
|
+
return {"error": "Either spec or spec_path must be provided"}
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
from loopengt.core.models.loop_spec import LoopSpec
|
|
85
|
+
from loopengt.core.runtime.executor import LoopExecutor
|
|
86
|
+
|
|
87
|
+
loop_spec = LoopSpec.model_validate(spec)
|
|
88
|
+
executor = LoopExecutor(loop_spec)
|
|
89
|
+
result = await executor.execute()
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"status": result.status.value,
|
|
93
|
+
"run_id": result.run_id,
|
|
94
|
+
"turns": result.turn,
|
|
95
|
+
"steps_completed": sum(
|
|
96
|
+
1
|
|
97
|
+
for s in result.step_states.values()
|
|
98
|
+
if s.status.value == "completed"
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
return {"error": str(exc)}
|
|
103
|
+
|
|
104
|
+
@server.tool()
|
|
105
|
+
async def resume_loop(
|
|
106
|
+
run_id: str,
|
|
107
|
+
checkpoint_id: str | None = None,
|
|
108
|
+
) -> dict[str, Any]:
|
|
109
|
+
"""Resume a paused or failed loop run.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
run_id: ID of the run to resume.
|
|
113
|
+
checkpoint_id: Optional specific checkpoint to resume from.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Resumption result with status.
|
|
117
|
+
"""
|
|
118
|
+
return {
|
|
119
|
+
"status": "resumed",
|
|
120
|
+
"run_id": run_id,
|
|
121
|
+
"checkpoint_id": checkpoint_id,
|
|
122
|
+
"note": "Full resume support requires checkpoint loading",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@server.tool()
|
|
126
|
+
async def list_runs(
|
|
127
|
+
runs_dir: str = ".loopengt/runs",
|
|
128
|
+
) -> dict[str, Any]:
|
|
129
|
+
"""List all loop runs.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
runs_dir: Directory containing run traces.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of run IDs and their statuses.
|
|
136
|
+
"""
|
|
137
|
+
runs_path = Path(runs_dir)
|
|
138
|
+
if not runs_path.exists():
|
|
139
|
+
return {"runs": [], "count": 0}
|
|
140
|
+
|
|
141
|
+
runs = []
|
|
142
|
+
for f in sorted(runs_path.glob("*.json")):
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
145
|
+
runs.append({
|
|
146
|
+
"run_id": data.get("run_id", f.stem),
|
|
147
|
+
"status": data.get("status", "unknown"),
|
|
148
|
+
"loop_name": data.get("loop_name", ""),
|
|
149
|
+
})
|
|
150
|
+
except Exception:
|
|
151
|
+
runs.append({"run_id": f.stem, "status": "unknown"})
|
|
152
|
+
|
|
153
|
+
return {"runs": runs, "count": len(runs)}
|
|
154
|
+
|
|
155
|
+
@server.tool()
|
|
156
|
+
async def inspect_trace(
|
|
157
|
+
run_id: str,
|
|
158
|
+
runs_dir: str = ".loopengt/runs",
|
|
159
|
+
) -> dict[str, Any]:
|
|
160
|
+
"""Inspect the execution trace of a run.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
run_id: Run ID to inspect.
|
|
164
|
+
runs_dir: Directory containing run traces.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The full trace data for the run.
|
|
168
|
+
"""
|
|
169
|
+
trace_path = Path(runs_dir) / f"{run_id}.json"
|
|
170
|
+
if not trace_path.exists():
|
|
171
|
+
return {"error": f"Trace not found: {run_id}"}
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
return json.loads(trace_path.read_text(encoding="utf-8"))
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
return {"error": f"Failed to read trace: {exc}"}
|
|
177
|
+
|
|
178
|
+
@server.tool()
|
|
179
|
+
async def evaluate_run(
|
|
180
|
+
run_id: str,
|
|
181
|
+
metrics: list[str] | None = None,
|
|
182
|
+
runs_dir: str = ".loopengt/runs",
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
"""Run evaluations against a completed loop run.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
run_id: Run ID to evaluate.
|
|
188
|
+
metrics: Specific metrics to evaluate (default: all).
|
|
189
|
+
runs_dir: Directory containing run traces.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Evaluation results with metric scores.
|
|
193
|
+
"""
|
|
194
|
+
trace_path = Path(runs_dir) / f"{run_id}.json"
|
|
195
|
+
if not trace_path.exists():
|
|
196
|
+
return {"error": f"Trace not found: {run_id}"}
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
trace_data = json.loads(trace_path.read_text(encoding="utf-8"))
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
return {"error": f"Failed to read trace: {exc}"}
|
|
202
|
+
|
|
203
|
+
from loopengt.cli.commands.eval import _evaluate_builtin_metrics
|
|
204
|
+
|
|
205
|
+
results = _evaluate_builtin_metrics(trace_data, metrics)
|
|
206
|
+
return {
|
|
207
|
+
"run_id": run_id,
|
|
208
|
+
"results": [
|
|
209
|
+
{"metric": name, "value": value, "status": status}
|
|
210
|
+
for name, value, status in results
|
|
211
|
+
],
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
logger.info("mcp.tools.registered", count=6)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP shared schemas subpackage."""
|