1. Fix `test_default_beads_path_auto_detection`
- Changed beads_path to use `Field(default_factory=_default_beads_path)` so the default is evaluated at instance
creation time, not class definition time
- Updated test to mock both `shutil.which` and `os.access`
2. Fix `test_init_creates_beads_directory`
- Fixed test to pass `working_dir=temp_dir` to `BdClient` instead of using `os.chdir()`
- The `_get_working_dir()` method checks `PWD` env var first, which isn't updated by `os.chdir()`
3. Fix minor linting errors reported by `ruff` tool
4. Update `beads` version to `0.9.6` in `uv.lock` file
MCP Server test coverage is now excellent, at 92% overall maintaining our high-standards of production level quality.
```
Name Stmts Miss Cover
------------------------------------------------
src/beads_mcp/__init__.py 1 0 100%
src/beads_mcp/__main__.py 3 3 0%
src/beads_mcp/bd_client.py 214 14 93%
src/beads_mcp/config.py 51 2 96%
src/beads_mcp/models.py 92 1 99%
src/beads_mcp/server.py 58 16 72%
src/beads_mcp/tools.py 59 0 100%
------------------------------------------------
TOTAL 478 36 92%
```
249 lines
7.9 KiB
Python
249 lines
7.9 KiB
Python
"""MCP tools for beads issue tracker."""
|
|
|
|
from typing import Annotated
|
|
|
|
from .bd_client import BdClient, BdError
|
|
from .models import (
|
|
AddDependencyParams,
|
|
BlockedIssue,
|
|
CloseIssueParams,
|
|
CreateIssueParams,
|
|
DependencyType,
|
|
InitParams,
|
|
Issue,
|
|
IssueStatus,
|
|
IssueType,
|
|
ListIssuesParams,
|
|
ReadyWorkParams,
|
|
ShowIssueParams,
|
|
Stats,
|
|
UpdateIssueParams,
|
|
)
|
|
|
|
# Global client instance - initialized on first use
|
|
_client: BdClient | None = None
|
|
_version_checked: bool = False
|
|
|
|
# Default constants
|
|
DEFAULT_ISSUE_TYPE: IssueType = "task"
|
|
DEFAULT_DEPENDENCY_TYPE: DependencyType = "blocks"
|
|
|
|
|
|
async def _get_client() -> BdClient:
|
|
"""Get a BdClient instance, creating it on first use.
|
|
|
|
Performs version check on first initialization.
|
|
|
|
Returns:
|
|
Configured BdClient instance (config loaded automatically)
|
|
|
|
Raises:
|
|
BdError: If bd is not installed or version is incompatible
|
|
"""
|
|
global _client, _version_checked
|
|
if _client is None:
|
|
_client = BdClient()
|
|
|
|
# Check version once per server lifetime
|
|
if not _version_checked:
|
|
await _client._check_version()
|
|
_version_checked = True
|
|
|
|
return _client
|
|
|
|
|
|
async def beads_ready_work(
|
|
limit: Annotated[int, "Maximum number of issues to return (1-100)"] = 10,
|
|
priority: Annotated[int | None, "Filter by priority (0-4, 0=highest)"] = None,
|
|
assignee: Annotated[str | None, "Filter by assignee"] = None,
|
|
) -> list[Issue]:
|
|
"""Find issues with no blocking dependencies that are ready to work on.
|
|
|
|
Ready work = status is 'open' AND no blocking dependencies.
|
|
Perfect for agents to claim next work!
|
|
"""
|
|
client = await _get_client()
|
|
params = ReadyWorkParams(limit=limit, priority=priority, assignee=assignee)
|
|
return await client.ready(params)
|
|
|
|
|
|
async def beads_list_issues(
|
|
status: Annotated[IssueStatus | None, "Filter by status (open, in_progress, blocked, closed)"] = None,
|
|
priority: Annotated[int | None, "Filter by priority (0-4, 0=highest)"] = None,
|
|
issue_type: Annotated[IssueType | None, "Filter by type (bug, feature, task, epic, chore)"] = None,
|
|
assignee: Annotated[str | None, "Filter by assignee"] = None,
|
|
limit: Annotated[int, "Maximum number of issues to return (1-1000)"] = 50,
|
|
) -> list[Issue]:
|
|
"""List all issues with optional filters."""
|
|
client = await _get_client()
|
|
|
|
params = ListIssuesParams(
|
|
status=status,
|
|
priority=priority,
|
|
issue_type=issue_type,
|
|
assignee=assignee,
|
|
limit=limit,
|
|
)
|
|
return await client.list_issues(params)
|
|
|
|
|
|
async def beads_show_issue(
|
|
issue_id: Annotated[str, "Issue ID (e.g., bd-1)"],
|
|
) -> Issue:
|
|
"""Show detailed information about a specific issue.
|
|
|
|
Includes full description, dependencies, and dependents.
|
|
"""
|
|
client = await _get_client()
|
|
params = ShowIssueParams(issue_id=issue_id)
|
|
return await client.show(params)
|
|
|
|
|
|
async def beads_create_issue(
|
|
title: Annotated[str, "Issue title"],
|
|
description: Annotated[str, "Issue description"] = "",
|
|
design: Annotated[str | None, "Design notes"] = None,
|
|
acceptance: Annotated[str | None, "Acceptance criteria"] = None,
|
|
external_ref: Annotated[str | None, "External reference (e.g., gh-9, jira-ABC)"] = None,
|
|
priority: Annotated[int, "Priority (0-4, 0=highest)"] = 2,
|
|
issue_type: Annotated[IssueType, "Type: bug, feature, task, epic, or chore"] = DEFAULT_ISSUE_TYPE,
|
|
assignee: Annotated[str | None, "Assignee username"] = None,
|
|
labels: Annotated[list[str] | None, "List of labels"] = None,
|
|
id: Annotated[str | None, "Explicit issue ID (e.g., bd-42)"] = None,
|
|
deps: Annotated[list[str] | None, "Dependencies (e.g., ['bd-20', 'blocks:bd-15'])"] = None,
|
|
) -> Issue:
|
|
"""Create a new issue.
|
|
|
|
Use this when you discover new work during your session.
|
|
Link it back with beads_add_dependency using 'discovered-from' type.
|
|
"""
|
|
client = await _get_client()
|
|
params = CreateIssueParams(
|
|
title=title,
|
|
description=description,
|
|
design=design,
|
|
acceptance=acceptance,
|
|
external_ref=external_ref,
|
|
priority=priority,
|
|
issue_type=issue_type,
|
|
assignee=assignee,
|
|
labels=labels or [],
|
|
id=id,
|
|
deps=deps or [],
|
|
)
|
|
return await client.create(params)
|
|
|
|
|
|
async def beads_update_issue(
|
|
issue_id: Annotated[str, "Issue ID (e.g., bd-1)"],
|
|
status: Annotated[IssueStatus | None, "New status (open, in_progress, blocked, closed)"] = None,
|
|
priority: Annotated[int | None, "New priority (0-4)"] = None,
|
|
assignee: Annotated[str | None, "New assignee"] = None,
|
|
title: Annotated[str | None, "New title"] = None,
|
|
design: Annotated[str | None, "Design notes"] = None,
|
|
acceptance_criteria: Annotated[str | None, "Acceptance criteria"] = None,
|
|
notes: Annotated[str | None, "Additional notes"] = None,
|
|
external_ref: Annotated[str | None, "External reference (e.g., gh-9, jira-ABC)"] = None,
|
|
) -> Issue:
|
|
"""Update an existing issue.
|
|
|
|
Claim work by setting status to 'in_progress'.
|
|
"""
|
|
client = await _get_client()
|
|
params = UpdateIssueParams(
|
|
issue_id=issue_id,
|
|
status=status,
|
|
priority=priority,
|
|
assignee=assignee,
|
|
title=title,
|
|
design=design,
|
|
acceptance_criteria=acceptance_criteria,
|
|
notes=notes,
|
|
external_ref=external_ref,
|
|
)
|
|
return await client.update(params)
|
|
|
|
|
|
async def beads_close_issue(
|
|
issue_id: Annotated[str, "Issue ID (e.g., bd-1)"],
|
|
reason: Annotated[str, "Reason for closing"] = "Completed",
|
|
) -> list[Issue]:
|
|
"""Close (complete) an issue.
|
|
|
|
Mark work as done when you've finished implementing/fixing it.
|
|
"""
|
|
client = await _get_client()
|
|
params = CloseIssueParams(issue_id=issue_id, reason=reason)
|
|
return await client.close(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)"],
|
|
dep_type: Annotated[
|
|
DependencyType,
|
|
"Dependency type: blocks, related, parent-child, or discovered-from",
|
|
] = DEFAULT_DEPENDENCY_TYPE,
|
|
) -> str:
|
|
"""Add a dependency relationship between two issues.
|
|
|
|
Types:
|
|
- blocks: to_id must complete before from_id can start
|
|
- related: Soft connection, doesn't block progress
|
|
- parent-child: Epic/subtask hierarchical relationship
|
|
- discovered-from: Track that from_id was discovered while working on to_id
|
|
|
|
Use 'discovered-from' when you find new work during your session.
|
|
"""
|
|
client = await _get_client()
|
|
params = AddDependencyParams(
|
|
from_id=from_id,
|
|
to_id=to_id,
|
|
dep_type=dep_type,
|
|
)
|
|
try:
|
|
await client.add_dependency(params)
|
|
return f"Added dependency: {from_id} depends on {to_id} ({dep_type})"
|
|
except BdError as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
async def beads_quickstart() -> str:
|
|
"""Get bd quickstart guide.
|
|
|
|
Read this first to understand how to use beads (bd) commands.
|
|
"""
|
|
client = await _get_client()
|
|
return await client.quickstart()
|
|
|
|
|
|
async def beads_stats() -> Stats:
|
|
"""Get statistics about issues.
|
|
|
|
Returns total issues, open, in_progress, closed, blocked, ready issues,
|
|
and average lead time in hours.
|
|
"""
|
|
client = await _get_client()
|
|
return await client.stats()
|
|
|
|
|
|
async def beads_blocked() -> list[BlockedIssue]:
|
|
"""Get blocked issues.
|
|
|
|
Returns issues that have blocking dependencies, showing what blocks them.
|
|
"""
|
|
client = await _get_client()
|
|
return await client.blocked()
|
|
|
|
|
|
async def beads_init(
|
|
prefix: Annotated[str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"] = None,
|
|
) -> str:
|
|
"""Initialize bd in current directory.
|
|
|
|
Creates .beads/ directory and database file with optional custom prefix.
|
|
"""
|
|
client = await _get_client()
|
|
params = InitParams(prefix=prefix)
|
|
return await client.init(params)
|