diff --git a/src/basic_memory/api/app.py b/src/basic_memory/api/app.py index 71641321..ba1d6931 100644 --- a/src/basic_memory/api/app.py +++ b/src/basic_memory/api/app.py @@ -82,6 +82,9 @@ async def lifespan(app: FastAPI): # pragma: no cover # Legacy web app proxy paths (compat with /proxy/projects/projects) app.include_router(v2_project, prefix="/proxy/projects") +# Legacy v1 compat: older CLI versions call GET /projects/projects +app.include_router(v2_project, prefix="/projects") + # V2 routers are the only public API surface diff --git a/src/basic_memory/cli/commands/mcp.py b/src/basic_memory/cli/commands/mcp.py index 764cd468..5e7de048 100644 --- a/src/basic_memory/cli/commands/mcp.py +++ b/src/basic_memory/cli/commands/mcp.py @@ -1,22 +1,14 @@ """MCP server command with streamable HTTP transport.""" import os -import typer from typing import Optional +import typer +from loguru import logger + from basic_memory.cli.app import app from basic_memory.config import ConfigManager, init_mcp_logging -# Import mcp instance (has lifespan that handles initialization and file sync) -from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover - -# Import mcp tools to register them -import basic_memory.mcp.tools # noqa: F401 # pragma: no cover - -# Import prompts to register them -import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover -from loguru import logger - @app.command() def mcp( @@ -47,6 +39,13 @@ def mcp( # Even when cloud_mode_enabled is True, stdio MCP runs locally and needs local API access. os.environ["BASIC_MEMORY_FORCE_LOCAL"] = "true" + # Deferred imports to avoid heavy startup cost for unrelated CLI commands + from basic_memory.mcp.server import mcp as mcp_server # pragma: no cover + + # Import mcp tools/prompts to register them with the server + import basic_memory.mcp.tools # noqa: F401 # pragma: no cover + import basic_memory.mcp.prompts # noqa: F401 # pragma: no cover + # Initialize logging for MCP (file only, stdout breaks protocol) init_mcp_logging() diff --git a/src/basic_memory/cli/main.py b/src/basic_memory/cli/main.py index 2e0041d7..1571eef2 100644 --- a/src/basic_memory/cli/main.py +++ b/src/basic_memory/cli/main.py @@ -1,22 +1,29 @@ """Main CLI entry point for basic-memory.""" # pragma: no cover +import sys + from basic_memory.cli.app import app # pragma: no cover -# Register commands -from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover - cloud, - db, - doctor, - import_chatgpt, - import_claude_conversations, - import_claude_projects, - import_memory_json, - mcp, - project, - status, - tool, -) +def _version_flag_present(argv: list[str]) -> bool: + return any(flag in argv for flag in ("--version", "-v")) + + +if not _version_flag_present(sys.argv[1:]): + # Register commands only when not short-circuiting for --version + from basic_memory.cli.commands import ( # noqa: F401 # pragma: no cover + cloud, + db, + doctor, + import_chatgpt, + import_claude_conversations, + import_claude_projects, + import_memory_json, + mcp, + project, + status, + tool, + ) # Re-apply warning filter AFTER all imports # (authlib adds a DeprecationWarning filter that overrides ours) import warnings # pragma: no cover diff --git a/tests/api/v2/test_project_router.py b/tests/api/v2/test_project_router.py index 9e10f6fa..5e7ba02e 100644 --- a/tests/api/v2/test_project_router.py +++ b/tests/api/v2/test_project_router.py @@ -340,3 +340,26 @@ async def test_resolve_project_empty_identifier(client: AsyncClient, v2_projects response = await client.post(f"{v2_projects_url}/resolve", json=resolve_data) assert response.status_code == 422 # Validation error + + +# --- Legacy v1 compatibility tests --- + + +@pytest.mark.asyncio +async def test_legacy_v1_list_projects_endpoint(client: AsyncClient, test_project: Project): + """Test that the legacy /projects/projects endpoint still works for older CLI versions. + + This endpoint was removed when we migrated to v2 but older versions of + basic-memory-cloud CLI still call it for `bm project list`. + """ + # The legacy v1 endpoint was at /projects/projects + response = await client.get("/projects/projects/") + + assert response.status_code == 200 + data = response.json() + assert "projects" in data + assert "default_project" in data + + # Verify the test project is in the list + project_names = [p["name"] for p in data["projects"]] + assert test_project.name in project_names