feat(mcp): Add reopen command support for closed issues

Implements the `bd` reopen command across the entire MCP stack, enabling
agents to reopen closed issues with optional reason tracking for audit
trails. This addresses the need to handle regressions and incorrectly
closed issues without manual `bd` CLI intervention.

The reopen command is more explicit than `bd update --status open` and
emits a dedicated Reopened event in the audit log, making it easier to
track why issues were reopened during analysis.

Changes:
  - `models.py`: Add ReopenIssueParams with issue_ids list and optional reason
  - `bd_client.py`: Implement reopen() method with JSON response parsing
  - `tools.py`: Add beads_reopen_issue() wrapper with Annotated types for MCP
  - `server.py`: Register 'reopen' MCP tool with description and parameters

Testing (10 new):
  - `test_bd_client.py`: 4 unit tests (mocked subprocess)
  - `test_bd_client_integration.py`: 3 integration tests (real `bd` CLI)
  - `test_mcp_server_integration.py`: 3 MCP integration tests (FastMCP Client)
  - `test_tools.py`: 3 tools wrapper tests (mocked BdClient)

Also updated `README.md`.
This commit is contained in:
Baishampayan Ghose
2025-10-16 18:08:37 +05:30
committed by Steve Yegge
parent 331a435418
commit 32a718dacd
10 changed files with 390 additions and 26 deletions

View File

@@ -15,6 +15,7 @@ from .models import (
Issue,
ListIssuesParams,
ReadyWorkParams,
ReopenIssueParams,
ShowIssueParams,
Stats,
UpdateIssueParams,
@@ -381,6 +382,26 @@ class BdClient:
return [Issue.model_validate(issue) for issue in data]
async def reopen(self, params: ReopenIssueParams) -> list[Issue]:
"""Reopen one or more closed issues.
Args:
params: Reopen parameters
Returns:
List of reopened issues
"""
args = ["reopen", *params.issue_ids]
if params.reason:
args.extend(["--reason", params.reason])
data = await self._run_command(*args)
if not isinstance(data, list):
raise BdCommandError(f"Invalid response for reopen {params.issue_ids}")
return [Issue.model_validate(issue) for issue in data]
async def add_dependency(self, params: AddDependencyParams) -> None:
"""Add a dependency between issues.

View File

@@ -86,6 +86,13 @@ class CloseIssueParams(BaseModel):
reason: str = "Completed"
class ReopenIssueParams(BaseModel):
"""Parameters for reopening issues."""
issue_ids: list[str]
reason: str | None = None
class AddDependencyParams(BaseModel):
"""Parameters for adding a dependency."""

View File

@@ -14,6 +14,7 @@ from beads_mcp.tools import (
beads_list_issues,
beads_quickstart,
beads_ready_work,
beads_reopen_issue,
beads_show_issue,
beads_stats,
beads_update_issue,
@@ -153,6 +154,15 @@ async def close_issue(issue_id: str, reason: str = "Completed") -> list[Issue]:
return await beads_close_issue(issue_id=issue_id, reason=reason)
@mcp.tool(
name="reopen",
description="Reopen one or more closed issues. Sets status to 'open' and clears closed_at timestamp.",
)
async def reopen_issue(issue_ids: list[str], reason: str | None = None) -> list[Issue]:
"""Reopen one or more closed issues."""
return await beads_reopen_issue(issue_ids=issue_ids, reason=reason)
@mcp.tool(
name="dep",
description="""Add a dependency between issues. Types: blocks (hard blocker),

View File

@@ -15,6 +15,7 @@ from .models import (
IssueType,
ListIssuesParams,
ReadyWorkParams,
ReopenIssueParams,
ShowIssueParams,
Stats,
UpdateIssueParams,
@@ -177,6 +178,20 @@ async def beads_close_issue(
return await client.close(params)
async def beads_reopen_issue(
issue_ids: Annotated[list[str], "Issue IDs to reopen (e.g., ['bd-1', 'bd-2'])"],
reason: Annotated[str | None, "Reason for reopening"] = None,
) -> list[Issue]:
"""Reopen one or more closed issues.
Sets status to 'open' and clears the closed_at timestamp.
More explicit than 'update --status open'.
"""
client = await _get_client()
params = ReopenIssueParams(issue_ids=issue_ids, reason=reason)
return await client.reopen(params)
async def beads_add_dependency(
from_id: Annotated[str, "Issue that depends on another (e.g., bd-2)"],
to_id: Annotated[str, "Issue that blocks or is related to from_id (e.g., bd-1)"],