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

97
backend/app/api/skills.py Normal file
View File

@@ -0,0 +1,97 @@
"""Agent skills API — list, view, edit, and preview skill matching."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from app.core.deps import require_admin, require_readonly
from app.database import get_db
from app.models.user import User
from app.schemas import SkillOut, SkillUpdate, SkillsListOut
from app.services.audit import record_audit
from app.services.skill_registry import (
get_skill,
list_skills_summary,
match_skills,
read_skill_raw,
save_skill_raw,
)
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/skills", tags=["skills"])
@router.get("", response_model=SkillsListOut)
async def list_skills(_: User = Depends(require_readonly)):
return SkillsListOut(
skills=list_skills_summary(),
path_hint="skills/",
)
@router.post("/preview")
async def preview_skill_match(
body: dict,
_: User = Depends(require_readonly),
):
"""Preview which skills match a task title + issue (and optional rule id)."""
title = str(body.get("title") or "")
issue = str(body.get("issue") or "")
matched_rule_id = body.get("matched_rule_id")
if matched_rule_id is not None:
matched_rule_id = str(matched_rule_id)
matched = match_skills(title, issue, matched_rule_id=matched_rule_id)
return {"matched": [s.as_dict() for s in matched]}
@router.get("/{skill_id}", response_model=SkillOut)
async def get_skill_detail(skill_id: str, _: User = Depends(require_readonly)):
skill = get_skill(skill_id)
if not skill:
raise HTTPException(status_code=404, detail="Skill not found")
return SkillOut(
id=skill["id"],
name=skill["name"],
description=skill["description"],
priority=skill.get("priority") or 0,
rule_ids=list(skill.get("rule_ids") or []),
match=skill.get("match") or {},
markdown=read_skill_raw(skill_id),
path_hint=f"skills/{skill_id}/SKILL.md",
)
@router.put("/{skill_id}", response_model=SkillOut)
async def update_skill(
skill_id: str,
body: SkillUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin),
):
try:
save_skill_raw(skill_id, body.markdown)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=400, detail=f"Invalid skill: {exc}") from exc
await record_audit(
db,
user,
"skills.update",
"skills",
0,
{"skill_id": skill_id, "bytes": len(body.markdown)},
)
skill = get_skill(skill_id)
if not skill:
raise HTTPException(status_code=404, detail="Skill not found")
return SkillOut(
id=skill["id"],
name=skill["name"],
description=skill["description"],
priority=skill.get("priority") or 0,
rule_ids=list(skill.get("rule_ids") or []),
match=skill.get("match") or {},
markdown=read_skill_raw(skill_id),
path_hint=f"skills/{skill_id}/SKILL.md",
)