Skip to content

Commit 6b0f19c

Browse files
committed
fix(mcp): include factory workspace metadata
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 49148fe commit 6b0f19c

4 files changed

Lines changed: 65 additions & 9 deletions

File tree

src/basic_memory/mcp/project_context.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,9 +439,10 @@ async def _fetch_workspace_project_entries(
439439
)
440440
entries: list[WorkspaceProjectEntry] = []
441441
for project in project_list.projects:
442-
if default_permalink and project.permalink == default_permalink:
443-
project.is_default = True
444-
entries.append(WorkspaceProjectEntry(workspace=workspace, project=project))
442+
entry_project = project
443+
if default_permalink and project.permalink == default_permalink and not project.is_default:
444+
entry_project = project.model_copy(update={"is_default": True})
445+
entries.append(WorkspaceProjectEntry(workspace=workspace, project=entry_project))
445446

446447
if context: # pragma: no cover
447448
await context.info(
@@ -483,6 +484,13 @@ async def _ensure_workspace_project_index(
483484
return index
484485

485486

487+
async def ensure_workspace_project_index(
488+
context: Optional[Context] = None,
489+
) -> WorkspaceProjectIndex:
490+
"""Public wrapper for loading the session-local workspace/project lookup index."""
491+
return await _ensure_workspace_project_index(context=context)
492+
493+
486494
async def resolve_workspace_project_identifier(
487495
project: str,
488496
context: Optional[Context] = None,

src/basic_memory/mcp/tools/project_management.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from basic_memory.mcp.async_client import get_client, get_cloud_proxy_client, is_factory_mode
1515
from basic_memory.mcp.project_context import (
1616
WorkspaceProjectEntry,
17-
_ensure_workspace_project_index,
17+
ensure_workspace_project_index,
1818
resolve_workspace_parameter,
1919
)
2020
from basic_memory.mcp.server import mcp
@@ -317,6 +317,8 @@ async def list_memory_projects(
317317
cloud_ws_name: str | None = None
318318
cloud_ws_type: str | None = None
319319
cloud_ws_tenant_id: str | None = None
320+
cloud_ws_slug: str | None = None
321+
cloud_ws_is_default = False
320322
try:
321323
from basic_memory.mcp.project_context import get_available_workspaces
322324

@@ -332,6 +334,8 @@ async def list_memory_projects(
332334
cloud_ws_name = matched.name
333335
cloud_ws_type = matched.workspace_type
334336
cloud_ws_tenant_id = matched.tenant_id
337+
cloud_ws_slug = matched.slug
338+
cloud_ws_is_default = matched.is_default
335339
except Exception:
336340
pass # workspace lookup is best-effort
337341

@@ -341,6 +345,8 @@ async def list_memory_projects(
341345
cloud_workspace_name=cloud_ws_name,
342346
cloud_workspace_type=cloud_ws_type,
343347
cloud_workspace_tenant_id=cloud_ws_tenant_id,
348+
cloud_workspace_slug=cloud_ws_slug,
349+
cloud_workspace_is_default=cloud_ws_is_default,
344350
)
345351
if output_format == "json":
346352
return _format_project_list_json(
@@ -376,7 +382,7 @@ async def list_memory_projects(
376382
cloud_ws_is_default = active_workspace.is_default
377383
else:
378384
try:
379-
workspace_index = await _ensure_workspace_project_index(context=context)
385+
workspace_index = await ensure_workspace_project_index(context=context)
380386
cloud_entries = workspace_index.entries
381387
except Exception as exc:
382388
logger.warning(

tests/mcp/test_project_context.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
from contextlib import asynccontextmanager
910
from typing import Any, AsyncIterator, cast
1011

1112
import pytest
@@ -424,6 +425,44 @@ async def fake_fetch_workspace_project_entries(workspace, context=None):
424425
assert calls == ["personal", "acme", "personal", "acme"]
425426

426427

428+
@pytest.mark.asyncio
429+
async def test_fetch_workspace_project_entries_copies_default_project(monkeypatch):
430+
import basic_memory.mcp.async_client as async_client
431+
from basic_memory.mcp.project_context import _fetch_workspace_project_entries
432+
from basic_memory.schemas.project_info import ProjectList
433+
434+
workspace = _workspace(
435+
tenant_id="personal-tenant",
436+
workspace_type="personal",
437+
slug="personal",
438+
name="Personal",
439+
role="owner",
440+
is_default=True,
441+
)
442+
project = _project("Default Notes", id=3, external_id="default-notes-id")
443+
project_list = ProjectList(projects=[project], default_project="Default Notes")
444+
445+
@asynccontextmanager
446+
async def fake_get_client(*args, **kwargs) -> AsyncIterator[object]:
447+
yield object()
448+
449+
async def fake_list_projects(self):
450+
return project_list
451+
452+
monkeypatch.setattr(async_client, "is_factory_mode", lambda: True)
453+
monkeypatch.setattr(async_client, "get_client", fake_get_client)
454+
monkeypatch.setattr(
455+
"basic_memory.mcp.clients.project.ProjectClient.list_projects",
456+
fake_list_projects,
457+
)
458+
459+
entries = await _fetch_workspace_project_entries(workspace)
460+
461+
assert project.is_default is False
462+
assert entries[0].project is not project
463+
assert entries[0].project.is_default is True
464+
465+
427466
@pytest.mark.asyncio
428467
async def test_resolve_workspace_project_identifier_handles_qualified_and_collisions(monkeypatch):
429468
import basic_memory.mcp.project_context as project_context

tests/mcp/test_tool_project_management.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ async def test_list_memory_projects_local_and_cloud_merge(app, test_project):
168168
return_value=True,
169169
),
170170
patch(
171-
"basic_memory.mcp.tools.project_management._ensure_workspace_project_index",
171+
"basic_memory.mcp.tools.project_management.ensure_workspace_project_index",
172172
new_callable=AsyncMock,
173173
return_value=workspace_index,
174174
),
@@ -207,7 +207,7 @@ async def test_list_memory_projects_cloud_failure_graceful(app, test_project):
207207
return_value=True,
208208
),
209209
patch(
210-
"basic_memory.mcp.tools.project_management._ensure_workspace_project_index",
210+
"basic_memory.mcp.tools.project_management.ensure_workspace_project_index",
211211
new_callable=AsyncMock,
212212
side_effect=RuntimeError("cloud unavailable"),
213213
),
@@ -283,6 +283,9 @@ async def test_list_memory_projects_factory_mode_json_includes_workspace(app, te
283283
assert proj["workspace_name"] == "My Org"
284284
assert proj["workspace_type"] == "organization"
285285
assert proj["workspace_tenant_id"] == "tenant-abc"
286+
assert proj["workspace_slug"] == "my-org"
287+
assert proj["workspace_is_default"] is False
288+
assert proj["qualified_name"] == "my-org/cloud-proj"
286289

287290

288291
@pytest.mark.asyncio
@@ -374,7 +377,7 @@ async def test_list_memory_projects_json_with_cloud(app, test_project):
374377
return_value=True,
375378
),
376379
patch(
377-
"basic_memory.mcp.tools.project_management._ensure_workspace_project_index",
380+
"basic_memory.mcp.tools.project_management.ensure_workspace_project_index",
378381
new_callable=AsyncMock,
379382
return_value=workspace_index,
380383
),
@@ -566,7 +569,7 @@ async def test_list_memory_projects_aggregates_without_config_workspace(app, tes
566569
return_value=True,
567570
),
568571
patch(
569-
"basic_memory.mcp.tools.project_management._ensure_workspace_project_index",
572+
"basic_memory.mcp.tools.project_management.ensure_workspace_project_index",
570573
new_callable=AsyncMock,
571574
return_value=workspace_index,
572575
) as mock_index,

0 commit comments

Comments
 (0)