Files
beads/integrations/beads-mcp/src/beads_mcp/tools.py
Baishampayan Ghose 4353592fe6 test(mcp): Fix two failing integration tests and linting errors
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%
```
2025-10-15 16:59:16 +05:30

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)