Enhance MCP development features and introduce skills management

- Added configuration options for requiring human approval before applying LLM-generated MCP patches.
- Updated Docker setup to include skills directory.
- Integrated skills management into the backend, allowing for procedural guides and skill matching.
- Refactored database initialization to apply Alembic migrations.
- Enhanced task approval process to handle MCP patch applications with optional approval.
- Introduced new schemas for skills and updated existing APIs to support skills functionality.

This commit lays the groundwork for improved agent capabilities and better management of MCP development processes.
This commit is contained in:
2026-06-14 22:27:24 +03:00
parent 6185b9b85a
commit 0375b20bb4
30 changed files with 1733 additions and 151 deletions

View File

@@ -10,6 +10,10 @@ from langchain_core.messages import HumanMessage, SystemMessage
from app.agent.json_utils import parse_json as _parse_json
from app.config import settings
from app.services import mcp_workspace
from app.services.mcp_approvals import (
backup_proposal_files,
execute_approved_patch,
)
from app.services.model_config import get_routing_config, resolve_tier_model
from app.services.model_router import build_chat_model
@@ -103,16 +107,67 @@ async def propose_mcp_patch(
return {"ok": True, **data}
async def apply_and_reload_mcp(
async def _queue_apply_patch_approval(
*,
task_id: int,
server: str,
edits: list[dict],
manager,
proposal: dict,
tool: str,
args: dict,
device: str | None,
emit: EmitFn,
) -> dict[str, Any]:
written = await mcp_workspace.apply_file_edits(server, edits)
if not written:
return {"ok": False, "error": "No files written"}
await manager.reload_server(server)
return {"ok": True, "files": written, "tools": mcp_workspace.list_registered_tools(server)}
from app.database import SessionLocal
from app.models.enums import ApprovalStatus
from app.models.task import ApprovalRequest
edits = proposal.get("files") or []
backups = await backup_proposal_files(server, edits)
commit_msg = proposal.get("commit_message") or f"agentic-os: fix {tool} on {server}"
approval_args = {
"server": server,
"files": edits,
"backups": backups,
"commit_message": commit_msg,
"summary": proposal.get("summary"),
"retry_context": {
"device": device,
"server": server,
"tool": tool,
"args": args,
},
}
async with SessionLocal() as db:
db.add(
ApprovalRequest(
task_id=task_id,
tool_name="mcp_apply_patch",
tool_args=approval_args,
risk=proposal.get("summary"),
status=ApprovalStatus.pending,
)
)
await db.commit()
await emit(
task_id,
"approval",
f"MCP patch proposed for {server} — approve to apply and reload",
{
"server": server,
"tool_name": "mcp_apply_patch",
"files": [e.get("path") for e in edits],
"summary": proposal.get("summary"),
},
)
return {
"ok": True,
"pending_approval": True,
"approval_type": "mcp_apply_patch",
"summary": proposal.get("summary"),
}
async def attempt_mcp_repair(
@@ -128,8 +183,9 @@ async def attempt_mcp_repair(
emit: EmitFn,
task_title: str = "",
push: bool | None = None,
device: str | None = None,
) -> dict[str, Any]:
"""Full repair loop: analyze → patch → reload → optional git push."""
"""Full repair loop: analyze → (approval?) → patch → reload → optional git push."""
if not settings.mcp_development_enabled:
return {"ok": False, "skipped": True, "reason": "MCP development disabled"}
@@ -152,6 +208,17 @@ async def attempt_mcp_repair(
await emit(task_id, "mcp_dev", f"MCP patch proposal failed: {proposal.get('error')}", proposal)
return proposal
if settings.mcp_dev_require_approval and task_id > 0:
return await _queue_apply_patch_approval(
task_id=task_id,
server=server,
proposal=proposal,
tool=tool,
args=args,
device=device,
emit=emit,
)
await emit(
task_id,
"mcp_dev",
@@ -166,66 +233,25 @@ async def attempt_mcp_repair(
{"server": server, "phase": "reload"},
)
reload_result = await apply_and_reload_mcp(server, proposal.get("files") or [], manager)
if not reload_result.get("ok"):
await emit(task_id, "error", reload_result.get("error", "MCP reload failed"), reload_result)
return reload_result
await emit(
task_id,
"mcp_dev",
f"MCP '{server}' reloaded — {len(reload_result.get('tools') or [])} tools registered",
{"server": server, "phase": "reload_done", "tools": reload_result.get("tools")},
return await execute_approved_patch(
task_id=task_id,
tool_args={
"server": server,
"files": proposal.get("files") or [],
"commit_message": proposal.get("commit_message") or f"agentic-os: fix {tool} on {server}",
"summary": proposal.get("summary"),
"retry_context": {
"device": device,
"server": server,
"tool": tool,
"args": args,
},
},
manager=manager,
emit=emit,
push=push,
)
should_push = push if push is not None else settings.mcp_dev_auto_push
push_result: dict[str, Any] | None = None
commit_msg = proposal.get("commit_message") or f"agentic-os: fix {tool} on {server}"
written = reload_result.get("files") or []
if should_push and settings.gitea_token:
push_result = await mcp_workspace.commit_and_push(server, commit_msg, written)
await emit(
task_id,
"mcp_dev",
f"Git push {'ok' if push_result.get('ok') else 'failed'}: {push_result.get('commit') or push_result.get('error')}",
{"server": server, "phase": "push", **push_result},
)
elif settings.gitea_token and written and task_id > 0:
from app.database import SessionLocal
from app.models.enums import ApprovalStatus
from app.models.task import ApprovalRequest
async with SessionLocal() as db:
db.add(
ApprovalRequest(
task_id=task_id,
tool_name="mcp_git_push",
tool_args={
"server": server,
"files": written,
"commit_message": commit_msg,
},
risk=proposal.get("summary"),
status=ApprovalStatus.pending,
)
)
await db.commit()
await emit(
task_id,
"approval",
f"MCP patch ready — approve to push {server} to Gitea",
{"server": server, "tool_name": "mcp_git_push", "files": written},
)
return {
"ok": True,
"summary": proposal.get("summary"),
"reload": reload_result,
"push": push_result,
"tools": reload_result.get("tools"),
}
async def maybe_repair_after_tool_failure(
*,
@@ -259,10 +285,14 @@ async def maybe_repair_after_tool_failure(
emit=emit,
task_title=task_title,
push=settings.mcp_dev_auto_push,
device=device,
)
if not result.get("ok"):
return None
if result.get("pending_approval"):
return None
if result.get("push") and not result["push"].get("ok") and result["push"].get("runtime_only"):
await emit(
task_id,
@@ -271,6 +301,10 @@ async def maybe_repair_after_tool_failure(
result["push"],
)
retry = result.get("retry")
if retry and retry.get("result") is not None:
return retry["result"], bool(retry.get("ok"))
from app.agent.mcp_helpers import tool_result_ok
from app.agent.tool_events import tool_event_payload
from app.mcp_manager.recovery import call_tool_resilient