fix: Remove unsupported engines field and add runtime version checking
Fixes Claude Code marketplace plugin installation failure (bd-183). Problem: The plugin.json manifest included an engines field (borrowed from npm) to specify minimum bd CLI version requirements. However, Claude Code's plugin manifest schema doesn't recognize this field, causing validation errors when installing via /plugin marketplace add. Solution: 1. Remove the engines field from plugin.json 2. Add runtime version checking in the MCP server startup 3. Update documentation to reflect automatic version checking Changes: - .claude-plugin/plugin.json: Remove unsupported engines field - integrations/beads-mcp/src/beads_mcp/bd_client.py: - Add BdVersionError exception class - Add _check_version() method to validate bd CLI >= 0.9.0 - Use bd version command (not bd --version) - integrations/beads-mcp/src/beads_mcp/tools.py: - Make _get_client() async to support version checking - Update all tool functions to await _get_client() - Add version check on first MCP server use - .claude-plugin/commands/bd-version.md: Update to mention automatic checking - PLUGIN.md: Document automatic version validation at startup Benefits: - Plugin installs successfully via Claude Code marketplace - Clear error messages if bd CLI version is too old - Version check happens once per MCP server lifetime (not per command) - Users get actionable update instructions in error messages Closes bd-183
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
|
||||
from .config import load_config
|
||||
from .models import (
|
||||
@@ -43,6 +44,12 @@ class BdCommandError(BdError):
|
||||
self.returncode = returncode
|
||||
|
||||
|
||||
class BdVersionError(BdError):
|
||||
"""Raised when bd version is incompatible with MCP server."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BdClient:
|
||||
"""Client for calling bd CLI commands and parsing JSON output."""
|
||||
|
||||
@@ -144,6 +151,57 @@ class BdClient:
|
||||
stderr=stdout_str,
|
||||
) from e
|
||||
|
||||
async def _check_version(self) -> None:
|
||||
"""Check that bd CLI version meets minimum requirements.
|
||||
|
||||
Raises:
|
||||
BdVersionError: If bd version is incompatible
|
||||
BdNotFoundError: If bd command not found
|
||||
"""
|
||||
# Minimum required version
|
||||
min_version = (0, 9, 0)
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
self.bd_path,
|
||||
"version",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
except FileNotFoundError as e:
|
||||
raise BdNotFoundError(
|
||||
f"bd command not found at '{self.bd_path}'. "
|
||||
f"Install bd from: https://github.com/steveyegge/beads"
|
||||
) from e
|
||||
|
||||
if process.returncode != 0:
|
||||
raise BdCommandError(
|
||||
f"bd version failed: {stderr.decode()}",
|
||||
stderr=stderr.decode(),
|
||||
returncode=process.returncode or 1,
|
||||
)
|
||||
|
||||
# Parse version from output like "bd version 0.9.2"
|
||||
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}"
|
||||
)
|
||||
|
||||
version = tuple(int(x) for x in match.groups())
|
||||
|
||||
if version < min_version:
|
||||
min_ver_str = ".".join(str(x) for x in min_version)
|
||||
cur_ver_str = ".".join(str(x) for x in version)
|
||||
install_cmd = "curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/install.sh | bash"
|
||||
raise BdVersionError(
|
||||
f"bd version {cur_ver_str} is too old. "
|
||||
f"This MCP server requires bd >= {min_ver_str}. "
|
||||
f"Update with: {install_cmd}"
|
||||
)
|
||||
|
||||
async def ready(self, params: ReadyWorkParams | None = None) -> list[Issue]:
|
||||
"""Get ready work (issues with no blocking dependencies).
|
||||
|
||||
|
||||
@@ -22,21 +22,33 @@ from .models import (
|
||||
|
||||
# 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"
|
||||
|
||||
|
||||
def _get_client() -> BdClient:
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@@ -50,7 +62,7 @@ async def beads_ready_work(
|
||||
Ready work = status is 'open' AND no blocking dependencies.
|
||||
Perfect for agents to claim next work!
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = ReadyWorkParams(limit=limit, priority=priority, assignee=assignee)
|
||||
return await client.ready(params)
|
||||
|
||||
@@ -67,7 +79,7 @@ async def beads_list_issues(
|
||||
limit: Annotated[int, "Maximum number of issues to return (1-1000)"] = 50,
|
||||
) -> list[Issue]:
|
||||
"""List all issues with optional filters."""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
|
||||
params = ListIssuesParams(
|
||||
status=status,
|
||||
@@ -86,7 +98,7 @@ async def beads_show_issue(
|
||||
|
||||
Includes full description, dependencies, and dependents.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = ShowIssueParams(issue_id=issue_id)
|
||||
return await client.show(params)
|
||||
|
||||
@@ -111,7 +123,7 @@ async def beads_create_issue(
|
||||
Use this when you discover new work during your session.
|
||||
Link it back with beads_add_dependency using 'discovered-from' type.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = CreateIssueParams(
|
||||
title=title,
|
||||
description=description,
|
||||
@@ -143,7 +155,7 @@ async def beads_update_issue(
|
||||
|
||||
Claim work by setting status to 'in_progress'.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = UpdateIssueParams(
|
||||
issue_id=issue_id,
|
||||
status=status,
|
||||
@@ -166,7 +178,7 @@ async def beads_close_issue(
|
||||
|
||||
Mark work as done when you've finished implementing/fixing it.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = CloseIssueParams(issue_id=issue_id, reason=reason)
|
||||
return await client.close(params)
|
||||
|
||||
@@ -189,7 +201,7 @@ async def beads_add_dependency(
|
||||
|
||||
Use 'discovered-from' when you find new work during your session.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = AddDependencyParams(
|
||||
from_id=from_id,
|
||||
to_id=to_id,
|
||||
@@ -207,7 +219,7 @@ async def beads_quickstart() -> str:
|
||||
|
||||
Read this first to understand how to use beads (bd) commands.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
return await client.quickstart()
|
||||
|
||||
|
||||
@@ -217,7 +229,7 @@ async def beads_stats() -> Stats:
|
||||
Returns total issues, open, in_progress, closed, blocked, ready issues,
|
||||
and average lead time in hours.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
return await client.stats()
|
||||
|
||||
|
||||
@@ -226,7 +238,7 @@ async def beads_blocked() -> list[BlockedIssue]:
|
||||
|
||||
Returns issues that have blocking dependencies, showing what blocks them.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
return await client.blocked()
|
||||
|
||||
|
||||
@@ -239,6 +251,6 @@ async def beads_init(
|
||||
|
||||
Creates .beads/ directory and database file with optional custom prefix.
|
||||
"""
|
||||
client = _get_client()
|
||||
client = await _get_client()
|
||||
params = InitParams(prefix=prefix)
|
||||
return await client.init(params)
|
||||
|
||||
Reference in New Issue
Block a user