Fix bd-8zf2: MCP server auto-detects workspace from CWD

- Add _find_beads_db_in_tree() to walk up looking for .beads/*.db
- Update _get_client() to auto-detect when workspace not set
- Matches CLI behavior (no manual set_context needed after restart)
- Add 8 comprehensive tests for auto-detection
- Update existing tests to mock auto-detection in error cases

Fixes silent failures after Amp restart.

Amp-Thread-ID: https://ampcode.com/threads/T-c47f524d-c101-40d5-839a-659f52b9be48
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-11-07 23:59:57 -08:00
parent e6e458fc40
commit 44179d7326
4 changed files with 228 additions and 11 deletions

View File

@@ -63,6 +63,50 @@ def _register_client_for_cleanup(client: BdClientBase) -> None:
pass
def _find_beads_db_in_tree(start_dir: str | None = None) -> str | None:
"""Walk up directory tree looking for .beads/*.db (matches Go CLI behavior).
Args:
start_dir: Starting directory (default: current working directory)
Returns:
Absolute path to workspace root containing .beads/*.db, or None if not found
"""
import glob
try:
current = os.path.abspath(start_dir or os.getcwd())
# Resolve symlinks like Go CLI does
try:
current = os.path.realpath(current)
except Exception:
pass
# Walk up directory tree
while True:
beads_dir = os.path.join(current, ".beads")
if os.path.isdir(beads_dir):
# Find any .db file in .beads/ (excluding backups)
db_files = glob.glob(os.path.join(beads_dir, "*.db"))
valid_dbs = [f for f in db_files if ".backup" not in os.path.basename(f)]
if valid_dbs:
# Return workspace root (parent of .beads), not the db path
return current
parent = os.path.dirname(current)
if parent == current: # Reached filesystem root
break
current = parent
return None
except Exception as e:
logger.debug(f"Failed to search for .beads in tree: {e}")
return None
def _resolve_workspace_root(path: str) -> str:
"""Resolve workspace root to git repo root if inside a git repo.
@@ -181,8 +225,11 @@ async def _get_client() -> BdClientBase:
"""Get a BdClient instance for the current workspace.
Uses connection pool to manage per-project daemon sockets.
Workspace is determined by current_workspace ContextVar or BEADS_WORKING_DIR env.
Workspace is auto-detected using the same logic as CLI:
1. current_workspace ContextVar (from workspace_root parameter)
2. BEADS_WORKING_DIR environment variable
3. Walk up from CWD looking for .beads/*.db
Performs health check before returning cached client.
On failure, drops from pool and attempts reconnection with exponential backoff.
@@ -193,13 +240,23 @@ async def _get_client() -> BdClientBase:
Configured BdClientBase instance for the current workspace
Raises:
BdError: If no workspace is set, or bd is not installed, or version is incompatible
BdError: If no workspace found, or bd is not installed, or version is incompatible
"""
# Determine workspace from ContextVar or environment
# Determine workspace using standard search order (matches Go CLI)
workspace = current_workspace.get() or os.environ.get("BEADS_WORKING_DIR")
# Auto-detect from CWD if not explicitly set (NEW!)
if not workspace:
workspace = _find_beads_db_in_tree()
if workspace:
logger.debug(f"Auto-detected workspace from CWD: {workspace}")
if not workspace:
raise BdError(
"No workspace set. Either provide workspace_root parameter or call set_context() first."
"No beads workspace found. Either:\n"
" 1. Call set_context(workspace_root=\"/path/to/project\"), OR\n"
" 2. Run from a directory containing .beads/, OR\n"
" 3. Set BEADS_WORKING_DIR environment variable"
)
# Canonicalize path to handle symlinks and deduplicate connections