diff --git a/integrations/beads-mcp/src/beads_mcp/bd_client.py b/integrations/beads-mcp/src/beads_mcp/bd_client.py index 60859e16..b6801a88 100644 --- a/integrations/beads-mcp/src/beads_mcp/bd_client.py +++ b/integrations/beads-mcp/src/beads_mcp/bd_client.py @@ -139,6 +139,43 @@ class BdClientBase(ABC): """Get current database schema for inspection.""" pass + @abstractmethod + async def repair_deps(self, fix: bool = False) -> dict: + """Find and optionally fix orphaned dependency references. + + Args: + fix: If True, automatically remove orphaned dependencies + + Returns: + Dict with orphans_found, orphans list, and fixed count if fix=True + """ + pass + + @abstractmethod + async def detect_pollution(self, clean: bool = False) -> dict: + """Detect test issues that leaked into production database. + + Args: + clean: If True, delete detected test issues + + Returns: + Dict with detected test issues and deleted count if clean=True + """ + pass + + @abstractmethod + async def validate(self, checks: str | None = None, fix_all: bool = False) -> dict: + """Run database validation checks. + + Args: + checks: Comma-separated list of checks (orphans,duplicates,pollution,conflicts) + fix_all: If True, auto-fix all fixable issues + + Returns: + Dict with validation results for each check + """ + pass + class BdCliClient(BdClientBase): """Client for calling bd CLI commands and parsing JSON output.""" @@ -623,6 +660,63 @@ class BdCliClient(BdClientBase): raise BdCommandError("Invalid response for get_schema_info") return data + async def repair_deps(self, fix: bool = False) -> dict: + """Find and optionally fix orphaned dependency references. + + Args: + fix: If True, automatically remove orphaned dependencies + + Returns: + Dict with orphans_found, orphans list, and fixed count if fix=True + """ + args = ["repair-deps"] + if fix: + args.append("--fix") + + data = await self._run_command(*args) + if not isinstance(data, dict): + raise BdCommandError("Invalid response for repair-deps") + return data + + async def detect_pollution(self, clean: bool = False) -> dict: + """Detect test issues that leaked into production database. + + Args: + clean: If True, delete detected test issues + + Returns: + Dict with detected test issues and deleted count if clean=True + """ + args = ["detect-pollution"] + if clean: + args.extend(["--clean", "--yes"]) + + data = await self._run_command(*args) + if not isinstance(data, dict): + raise BdCommandError("Invalid response for detect-pollution") + return data + + async def validate(self, checks: str | None = None, fix_all: bool = False) -> dict: + """Run database validation checks. + + Args: + checks: Comma-separated list of checks (orphans,duplicates,pollution,conflicts) + fix_all: If True, auto-fix all fixable issues + + Returns: + Dict with validation results for each check + """ + args = ["validate"] + if checks: + args.extend(["--checks", checks]) + if fix_all: + args.append("--fix-all") + + data = await self._run_command(*args) + if not isinstance(data, dict): + raise BdCommandError("Invalid response for validate") + return data + async def init(self, params: InitParams | None = None) -> str: """Initialize bd in current directory. diff --git a/integrations/beads-mcp/src/beads_mcp/bd_daemon_client.py b/integrations/beads-mcp/src/beads_mcp/bd_daemon_client.py index 735243aa..8f174ddc 100644 --- a/integrations/beads-mcp/src/beads_mcp/bd_daemon_client.py +++ b/integrations/beads-mcp/src/beads_mcp/bd_daemon_client.py @@ -452,6 +452,49 @@ class BdDaemonClient(BdClientBase): """ raise NotImplementedError("get_schema_info not supported via daemon - use CLI client") + async def repair_deps(self, fix: bool = False) -> dict: + """Find and optionally fix orphaned dependency references. + + Args: + fix: If True, automatically remove orphaned dependencies + + Returns: + Dict with orphans_found, orphans list, and fixed count if fix=True + + Note: + This falls back to CLI since repair operations are rare + """ + raise NotImplementedError("repair_deps not supported via daemon - use CLI client") + + async def detect_pollution(self, clean: bool = False) -> dict: + """Detect test issues that leaked into production database. + + Args: + clean: If True, delete detected test issues + + Returns: + Dict with detected test issues and deleted count if clean=True + + Note: + This falls back to CLI since pollution detection is a rare operation + """ + raise NotImplementedError("detect_pollution not supported via daemon - use CLI client") + + async def validate(self, checks: str | None = None, fix_all: bool = False) -> dict: + """Run database validation checks. + + Args: + checks: Comma-separated list of checks (orphans,duplicates,pollution,conflicts) + fix_all: If True, auto-fix all fixable issues + + Returns: + Dict with validation results for each check + + Note: + This falls back to CLI since validation is a rare operation + """ + raise NotImplementedError("validate not supported via daemon - use CLI client") + async def add_dependency(self, params: AddDependencyParams) -> None: """Add a dependency between issues. diff --git a/integrations/beads-mcp/src/beads_mcp/server.py b/integrations/beads-mcp/src/beads_mcp/server.py index e5a5ec26..5369e3ed 100644 --- a/integrations/beads-mcp/src/beads_mcp/server.py +++ b/integrations/beads-mcp/src/beads_mcp/server.py @@ -19,16 +19,19 @@ from beads_mcp.tools import ( beads_blocked, beads_close_issue, beads_create_issue, + beads_detect_pollution, beads_get_schema_info, beads_init, beads_inspect_migration, beads_list_issues, beads_quickstart, beads_ready_work, + beads_repair_deps, beads_reopen_issue, beads_show_issue, beads_stats, beads_update_issue, + beads_validate, current_workspace, # ContextVar for per-request workspace routing ) @@ -602,6 +605,53 @@ async def get_schema_info(workspace_root: str | None = None) -> dict: return await beads_get_schema_info() +@mcp.tool( + name="repair_deps", + description="Find and optionally fix orphaned dependency references.", +) +@with_workspace +async def repair_deps(fix: bool = False, workspace_root: str | None = None) -> dict: + """Find and optionally fix orphaned dependency references. + + Scans all issues for dependencies pointing to non-existent issues. + Returns orphaned dependencies and optionally removes them with fix=True. + """ + return await beads_repair_deps(fix=fix) + + +@mcp.tool( + name="detect_pollution", + description="Detect test issues that leaked into production database.", +) +@with_workspace +async def detect_pollution(clean: bool = False, workspace_root: str | None = None) -> dict: + """Detect test issues that leaked into production database. + + Detects test issues using pattern matching (titles starting with 'test', etc.). + Returns detected test issues and optionally deletes them with clean=True. + """ + return await beads_detect_pollution(clean=clean) + + +@mcp.tool( + name="validate", + description="Run comprehensive database health checks.", +) +@with_workspace +async def validate( + checks: str | None = None, + fix_all: bool = False, + workspace_root: str | None = None, +) -> dict: + """Run comprehensive database health checks. + + Available checks: orphans, duplicates, pollution, conflicts. + If checks is None, runs all checks. + Returns validation results for each check. + """ + return await beads_validate(checks=checks, fix_all=fix_all) + + async def async_main() -> None: """Async entry point for the MCP server.""" await mcp.run_async(transport="stdio") diff --git a/integrations/beads-mcp/src/beads_mcp/tools.py b/integrations/beads-mcp/src/beads_mcp/tools.py index 6d29175c..39838e0c 100644 --- a/integrations/beads-mcp/src/beads_mcp/tools.py +++ b/integrations/beads-mcp/src/beads_mcp/tools.py @@ -484,6 +484,60 @@ async def beads_get_schema_info() -> dict: return await client.get_schema_info() +async def beads_repair_deps( + fix: Annotated[bool, "If True, automatically remove orphaned dependencies"] = False, +) -> dict: + """Find and optionally fix orphaned dependency references. + + Scans all issues for dependencies pointing to non-existent issues. + Returns orphaned dependencies and optionally removes them with fix=True. + + Returns dict with: + - orphans_found: number of orphaned dependencies + - orphans: list of orphaned dependency details + - fixed: number of orphans fixed (if fix=True) + """ + client = await _get_client() + return await client.repair_deps(fix=fix) + + +async def beads_detect_pollution( + clean: Annotated[bool, "If True, delete detected test issues"] = False, +) -> dict: + """Detect test issues that leaked into production database. + + Detects test issues using pattern matching: + - Titles starting with 'test', 'benchmark', 'sample', 'tmp', 'temp' + - Sequential numbering (test-1, test-2, ...) + - Generic descriptions or no description + - Created in rapid succession + + Returns dict with detected test issues and deleted count if clean=True. + """ + client = await _get_client() + return await client.detect_pollution(clean=clean) + + +async def beads_validate( + checks: Annotated[str | None, "Comma-separated list of checks (orphans,duplicates,pollution,conflicts)"] = None, + fix_all: Annotated[bool, "If True, auto-fix all fixable issues"] = False, +) -> dict: + """Run comprehensive database health checks. + + Available checks: + - orphans: Orphaned dependencies (references to deleted issues) + - duplicates: Duplicate issues (identical content) + - pollution: Test pollution (leaked test issues) + - conflicts: Git merge conflicts in JSONL + + If checks is None, runs all checks. + + Returns dict with validation results for each check. + """ + client = await _get_client() + return await client.validate(checks=checks, fix_all=fix_all) + + async def beads_init( prefix: Annotated[str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"] = None, ) -> str: