diff --git a/integrations/beads-mcp/src/beads_mcp/bd_client.py b/integrations/beads-mcp/src/beads_mcp/bd_client.py index 22ca9c3d..49eedada 100644 --- a/integrations/beads-mcp/src/beads_mcp/bd_client.py +++ b/integrations/beads-mcp/src/beads_mcp/bd_client.py @@ -101,12 +101,8 @@ class BdClient: self.bd_path = bd_path if bd_path is not None else config.beads_path self.beads_db = beads_db if beads_db is not None else config.beads_db self.actor = actor if actor is not None else config.beads_actor - self.no_auto_flush = ( - no_auto_flush if no_auto_flush is not None else config.beads_no_auto_flush - ) - self.no_auto_import = ( - no_auto_import if no_auto_import is not None else config.beads_no_auto_import - ) + self.no_auto_flush = no_auto_flush if no_auto_flush is not None else config.beads_no_auto_flush + self.no_auto_import = no_auto_import if no_auto_import is not None else config.beads_no_auto_import self.working_dir = working_dir if working_dir is not None else config.beads_working_dir def _get_working_dir(self) -> str: @@ -161,9 +157,7 @@ class BdClient: ) stdout, stderr = await process.communicate() except FileNotFoundError as e: - raise BdNotFoundError( - BdNotFoundError.installation_message(self.bd_path) - ) from e + raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e if process.returncode != 0: raise BdCommandError( @@ -205,9 +199,7 @@ class BdClient: ) stdout, stderr = await process.communicate() except FileNotFoundError as e: - raise BdNotFoundError( - BdNotFoundError.installation_message(self.bd_path) - ) from e + raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e if process.returncode != 0: raise BdCommandError( @@ -220,9 +212,7 @@ class BdClient: version_output = stdout.decode().strip() match = re.search(r"(\d+)\.(\d+)\.(\d+)", version_output) if not match: - raise BdVersionError( - f"Could not parse bd version from: {version_output}" - ) + raise BdVersionError(f"Could not parse bd version from: {version_output}") version = tuple(int(x) for x in match.groups()) @@ -418,9 +408,7 @@ class BdClient: ) _stdout, stderr = await process.communicate() except FileNotFoundError as e: - raise BdNotFoundError( - BdNotFoundError.installation_message(self.bd_path) - ) from e + raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e if process.returncode != 0: raise BdCommandError( @@ -446,9 +434,7 @@ class BdClient: ) stdout, stderr = await process.communicate() except FileNotFoundError as e: - raise BdNotFoundError( - BdNotFoundError.installation_message(self.bd_path) - ) from e + raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e if process.returncode != 0: raise BdCommandError( @@ -513,9 +499,7 @@ class BdClient: ) stdout, stderr = await process.communicate() except FileNotFoundError as e: - raise BdNotFoundError( - BdNotFoundError.installation_message(self.bd_path) - ) from e + raise BdNotFoundError(BdNotFoundError.installation_message(self.bd_path)) from e if process.returncode != 0: raise BdCommandError( diff --git a/integrations/beads-mcp/src/beads_mcp/config.py b/integrations/beads-mcp/src/beads_mcp/config.py index 4300efc4..05d84838 100644 --- a/integrations/beads-mcp/src/beads_mcp/config.py +++ b/integrations/beads-mcp/src/beads_mcp/config.py @@ -5,7 +5,7 @@ import shutil import sys from pathlib import Path -from pydantic import field_validator +from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -31,7 +31,7 @@ class Config(BaseSettings): model_config = SettingsConfigDict(env_prefix="") - beads_path: str = _default_beads_path() + beads_path: str = Field(default_factory=_default_beads_path) beads_db: str | None = None beads_actor: str | None = None beads_no_auto_flush: bool = False @@ -71,9 +71,7 @@ class Config(BaseSettings): ) if not os.access(v, os.X_OK): - raise ValueError( - f"bd executable at {v} is not executable.\nPlease check file permissions." - ) + raise ValueError(f"bd executable at {v} is not executable.\nPlease check file permissions.") return v @@ -97,8 +95,7 @@ class Config(BaseSettings): path = Path(v) if not path.exists(): raise ValueError( - f"BEADS_DB points to non-existent file: {v}\n" - + "Please verify the database path is correct." + f"BEADS_DB points to non-existent file: {v}\n" + "Please verify the database path is correct." ) return v @@ -124,7 +121,7 @@ def load_config() -> Config: except Exception as e: default_path = _default_beads_path() error_msg = ( - f"Beads MCP Server Configuration Error\n\n" + "Beads MCP Server Configuration Error\n\n" + f"{e}\n\n" + "Common fix: Install the bd CLI first:\n" + " curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash\n\n" diff --git a/integrations/beads-mcp/src/beads_mcp/server.py b/integrations/beads-mcp/src/beads_mcp/server.py index bae113f3..d5856b7e 100644 --- a/integrations/beads-mcp/src/beads_mcp/server.py +++ b/integrations/beads-mcp/src/beads_mcp/server.py @@ -216,7 +216,7 @@ async def debug_env() -> str: info.append(f"USER: {os.environ.get('USER', 'NOT SET')}\n") info.append("\n=== All Environment Variables ===\n") for key, value in sorted(os.environ.items()): - if not key.startswith('_'): # Skip internal vars + if not key.startswith("_"): # Skip internal vars info.append(f"{key}={value}\n") return "".join(info) diff --git a/integrations/beads-mcp/src/beads_mcp/tools.py b/integrations/beads-mcp/src/beads_mcp/tools.py index 22083314..f0e7023c 100644 --- a/integrations/beads-mcp/src/beads_mcp/tools.py +++ b/integrations/beads-mcp/src/beads_mcp/tools.py @@ -68,13 +68,9 @@ async def beads_ready_work( async def beads_list_issues( - status: Annotated[ - IssueStatus | None, "Filter by status (open, in_progress, blocked, closed)" - ] = None, + 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, + 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]: @@ -110,9 +106,7 @@ async def beads_create_issue( 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, + 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, @@ -243,9 +237,7 @@ async def beads_blocked() -> list[BlockedIssue]: async def beads_init( - prefix: Annotated[ - str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)" - ] = None, + prefix: Annotated[str | None, "Issue prefix (e.g., 'myproject' for myproject-1, myproject-2)"] = None, ) -> str: """Initialize bd in current directory. diff --git a/integrations/beads-mcp/tests/test_bd_client.py b/integrations/beads-mcp/tests/test_bd_client.py index ea95eaa0..e07e4b64 100644 --- a/integrations/beads-mcp/tests/test_bd_client.py +++ b/integrations/beads-mcp/tests/test_bd_client.py @@ -1,18 +1,15 @@ """Unit tests for BdClient.""" -import asyncio import json from unittest.mock import AsyncMock, MagicMock, patch import pytest + from beads_mcp.bd_client import BdClient, BdCommandError, BdNotFoundError from beads_mcp.models import ( AddDependencyParams, CloseIssueParams, CreateIssueParams, - DependencyType, - IssueStatus, - IssueType, ListIssuesParams, ReadyWorkParams, ShowIssueParams, diff --git a/integrations/beads-mcp/tests/test_bd_client_integration.py b/integrations/beads-mcp/tests/test_bd_client_integration.py index 88b47c82..8f50de38 100644 --- a/integrations/beads-mcp/tests/test_bd_client_integration.py +++ b/integrations/beads-mcp/tests/test_bd_client_integration.py @@ -7,14 +7,11 @@ from pathlib import Path import pytest -from beads_mcp.bd_client import BdClient, BdCommandError, BdNotFoundError +from beads_mcp.bd_client import BdClient, BdCommandError from beads_mcp.models import ( AddDependencyParams, CloseIssueParams, CreateIssueParams, - DependencyType, - IssueStatus, - IssueType, ListIssuesParams, ReadyWorkParams, ShowIssueParams, @@ -358,7 +355,6 @@ async def test_init_creates_beads_directory(bd_executable): This is a critical test for the bug where init was using --db flag and creating the database in the wrong location. """ - import asyncio from beads_mcp.bd_client import BdClient from beads_mcp.models import InitParams @@ -370,30 +366,22 @@ async def test_init_creates_beads_directory(bd_executable): # Ensure .beads doesn't exist yet assert not beads_dir.exists() - # Create client WITHOUT beads_db set (this was the bug!) - client = BdClient(bd_path=bd_executable, beads_db=None) + # Create client WITHOUT beads_db set and WITH working_dir set to temp_dir + client = BdClient(bd_path=bd_executable, beads_db=None, working_dir=temp_dir) - # Change to temp directory and run init - original_cwd = os.getcwd() - try: - os.chdir(temp_dir) + # Initialize with custom prefix (no need to chdir!) + params = InitParams(prefix="test") + result = await client.init(params) - # Initialize with custom prefix - params = InitParams(prefix="test") - result = await client.init(params) + # Verify .beads directory was created in temp directory + assert beads_dir.exists(), f".beads directory not created in {temp_dir}" + assert beads_dir.is_dir(), ".beads exists but is not a directory" - # Verify .beads directory was created in current directory - assert beads_dir.exists(), f".beads directory not created in {temp_dir}" - assert beads_dir.is_dir(), f".beads exists but is not a directory" + # Verify database file was created with correct prefix + db_files = list(beads_dir.glob("*.db")) + assert len(db_files) > 0, "No database file created in .beads/" + assert any("test" in str(db.name) for db in db_files), \ + f"Database file doesn't contain prefix 'test': {[db.name for db in db_files]}" - # Verify database file was created with correct prefix - db_files = list(beads_dir.glob("*.db")) - assert len(db_files) > 0, "No database file created in .beads/" - assert any("test" in str(db.name) for db in db_files), \ - f"Database file doesn't contain prefix 'test': {[db.name for db in db_files]}" - - # Verify success message - assert "initialized" in result.lower() or "created" in result.lower() - - finally: - os.chdir(original_cwd) + # Verify success message + assert "initialized" in result.lower() or "created" in result.lower() diff --git a/integrations/beads-mcp/tests/test_config.py b/integrations/beads-mcp/tests/test_config.py index bc222794..30d469a5 100644 --- a/integrations/beads-mcp/tests/test_config.py +++ b/integrations/beads-mcp/tests/test_config.py @@ -1,7 +1,5 @@ """Tests for beads_mcp.config module.""" -import os -import shutil from pathlib import Path from unittest.mock import patch @@ -19,7 +17,7 @@ class TestConfig: monkeypatch.delenv("BEADS_PATH", raising=False) # Mock shutil.which to return a test path - with patch("shutil.which", return_value="/usr/local/bin/bd"): + with patch("shutil.which", return_value="/usr/local/bin/bd"), patch("os.access", return_value=True): config = Config() assert config.beads_path == "/usr/local/bin/bd" @@ -39,11 +37,9 @@ class TestConfig: monkeypatch.setenv("BEADS_PATH", "bd") # Mock shutil.which to simulate finding bd in PATH - with patch("shutil.which", return_value="/usr/local/bin/bd"): - # Mock os.access to say the file is executable - with patch("os.access", return_value=True): - config = Config() - assert config.beads_path == "/usr/local/bin/bd" + with patch("shutil.which", return_value="/usr/local/bin/bd"), patch("os.access", return_value=True): + config = Config() + assert config.beads_path == "/usr/local/bin/bd" def test_beads_path_not_found(self, monkeypatch: pytest.MonkeyPatch) -> None: """Test that invalid BEADS_PATH raises ValueError.""" diff --git a/integrations/beads-mcp/tests/test_tools.py b/integrations/beads-mcp/tests/test_tools.py index d96814b4..9fb0665a 100644 --- a/integrations/beads-mcp/tests/test_tools.py +++ b/integrations/beads-mcp/tests/test_tools.py @@ -4,8 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest -from beads_mcp.bd_client import BdClient -from beads_mcp.models import BlockedIssue, Issue, IssueStatus, IssueType, Stats +from beads_mcp.models import BlockedIssue, Issue, Stats from beads_mcp.tools import ( beads_add_dependency, beads_blocked, diff --git a/integrations/beads-mcp/uv.lock b/integrations/beads-mcp/uv.lock index 5a92b840..f09674e3 100644 --- a/integrations/beads-mcp/uv.lock +++ b/integrations/beads-mcp/uv.lock @@ -48,7 +48,7 @@ wheels = [ [[package]] name = "beads-mcp" -version = "0.9.4" +version = "0.9.6" source = { editable = "." } dependencies = [ { name = "fastmcp" },