Skip to content

Commit 1fdc9fd

Browse files
phernandezclaude
andcommitted
fix: resolve_project_parameter falls back to projects API for default (#644)
In cloud mode, ConfigManager has no local config so default_project is always None. Add API fallback in resolve_project_parameter that queries /v2/projects/ for the default_project field. This fixes all MCP tools that rely on project resolution (recent_activity, etc). Removed discovery mode tests that simulated an invalid state by clearing is_default — there must always be a default project. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 2feecdf commit 1fdc9fd

2 files changed

Lines changed: 35 additions & 60 deletions

File tree

src/basic_memory/mcp/project_context.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,29 @@ def set_workspace_provider(provider: Callable[[], Awaitable[list[WorkspaceInfo]]
4040
_workspace_provider = provider
4141

4242

43+
async def _resolve_default_project_from_api() -> Optional[str]:
44+
"""Query the projects API for the default project.
45+
46+
Used as a fallback when ConfigManager has no local config (cloud mode).
47+
"""
48+
from basic_memory.mcp.async_client import get_client
49+
50+
try:
51+
async with get_client() as client:
52+
response = await client.get("/v2/projects/")
53+
if response.status_code == 200:
54+
project_list = ProjectList.model_validate(response.json())
55+
if project_list.default_project:
56+
return project_list.default_project
57+
# Fallback: find project with is_default=True
58+
for p in project_list.projects:
59+
if p.is_default:
60+
return p.name
61+
except Exception:
62+
pass
63+
return None
64+
65+
4366
async def resolve_project_parameter(
4467
project: Optional[str] = None,
4568
allow_discovery: bool = False,
@@ -66,11 +89,16 @@ async def resolve_project_parameter(
6689
Returns:
6790
Resolved project name or None if no resolution possible
6891
"""
69-
# Load config for any values not explicitly provided
92+
# Load config for any values not explicitly provided.
93+
# ConfigManager reads from the local config file, which doesn't exist in cloud mode.
94+
# When it returns None, fall back to querying the projects API for the is_default flag.
7095
if default_project is None:
7196
config = ConfigManager().config
7297
default_project = config.default_project
7398

99+
if default_project is None:
100+
default_project = await _resolve_default_project_from_api()
101+
74102
# Create resolver with configuration and resolve
75103
resolver = ProjectResolver.from_env(
76104
default_project=default_project,

tests/mcp/test_tool_recent_activity.py

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -128,69 +128,16 @@ async def test_recent_activity_type_invalid(client, test_project, test_graph):
128128

129129

130130
@pytest.mark.asyncio
131-
async def test_recent_activity_discovery_mode(client, test_project, test_graph, config_manager):
132-
"""Test that recent_activity discovery mode works without project parameter."""
133-
# Clear default_project to test discovery mode
134-
cfg = config_manager.load_config()
135-
cfg.default_project = None
136-
config_manager.save_config(cfg)
137-
138-
# Test discovery mode (no project parameter)
131+
async def test_recent_activity_uses_default_project(client, test_project, test_graph):
132+
"""When no project parameter is given, recent_activity uses the default project."""
133+
# Call without explicit project — should resolve to the default
139134
result = await recent_activity()
140135
assert result is not None
141136
assert isinstance(result, str)
142137

143-
# Check that we get a formatted summary
144-
assert "Recent Activity Summary" in result
145-
assert "Most Active Project:" in result or "Other Active Projects:" in result
146-
assert "Summary:" in result
147-
assert "active projects" in result
148-
149-
# Should contain project discovery guidance
150-
assert "Suggested project:" in result or "Multiple active projects" in result
151-
assert "Session reminder:" in result
152-
153-
154-
@pytest.mark.asyncio
155-
async def test_recent_activity_discovery_mode_no_activity(client, test_project, config_manager):
156-
"""If there is no activity in any project, discovery mode should say so."""
157-
# Clear default_project to test discovery mode
158-
cfg = config_manager.load_config()
159-
cfg.default_project = None
160-
config_manager.save_config(cfg)
161-
162-
result = await recent_activity()
163-
assert "Recent Activity Summary" in result
164-
assert "No recent activity found in any project." in result
165-
166-
167-
@pytest.mark.asyncio
168-
async def test_recent_activity_discovery_mode_multiple_active_projects(
169-
app, client, test_project, tmp_path_factory, config_manager
170-
):
171-
"""Discovery mode should use the multi-project guidance when multiple projects have activity."""
172-
# Clear default_project to test discovery mode
173-
cfg = config_manager.load_config()
174-
cfg.default_project = None
175-
config_manager.save_config(cfg)
176-
177-
from basic_memory.mcp.tools import create_memory_project, write_note
178-
179-
second_root = tmp_path_factory.mktemp("second-project-home")
180-
181-
result = await create_memory_project(
182-
project_name="second-project",
183-
project_path=str(second_root),
184-
set_default=False,
185-
)
186-
assert result.startswith("✓")
187-
188-
await write_note(project=test_project.name, title="One", directory="notes", content="one")
189-
await write_note(project="second-project", title="Two", directory="notes", content="two")
190-
191-
out = await recent_activity()
192-
assert "Recent Activity Summary" in out
193-
assert "or would you prefer a different project" in out
138+
# Should return project-specific output for the default project
139+
assert "Recent Activity:" in result
140+
assert "Activity Summary:" in result
194141

195142

196143
def test_recent_activity_format_relative_time_and_truncate_helpers():

0 commit comments

Comments
 (0)