Skip to content

Commit 45b3dff

Browse files
committed
fix(mcp): use workspace provider for permalink context
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 76835c4 commit 45b3dff

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

src/basic_memory/mcp/project_context.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ async def _workspace_metadata_by_tenant_id(
833833
tenant_id: str,
834834
context: Optional[Context] = None,
835835
) -> WorkspaceInfo | None:
836-
"""Return cached workspace metadata for a configured tenant id."""
836+
"""Return non-index workspace metadata for a configured tenant id."""
837837
cached_workspace = await _get_cached_active_workspace(context)
838838
if cached_workspace and cached_workspace.tenant_id == tenant_id:
839839
return cached_workspace
@@ -845,6 +845,37 @@ async def _workspace_metadata_by_tenant_id(
845845
# Outcome: drop stale metadata and route without permalink decoration.
846846
await context.set_state("active_workspace", None)
847847

848+
if context:
849+
cached_raw = await context.get_state("available_workspaces")
850+
if isinstance(cached_raw, list):
851+
for item in cached_raw:
852+
if not isinstance(item, dict):
853+
continue
854+
workspace = WorkspaceInfo.model_validate(item)
855+
if workspace.tenant_id == tenant_id:
856+
return workspace
857+
858+
if _workspace_provider is not None:
859+
# Trigger: the hosting runtime can provide workspace metadata directly.
860+
# Why: configured workspace_id is already sufficient for tenant routing, but
861+
# canonical organization permalinks also need slug/type context.
862+
# Outcome: use the injected runtime seam without loading the workspace project index.
863+
workspace = next(
864+
(
865+
workspace
866+
for workspace in await get_available_workspaces(context=context)
867+
if workspace.tenant_id == tenant_id
868+
),
869+
None,
870+
)
871+
if workspace is None:
872+
raise ValueError(
873+
f"Configured workspace_id '{tenant_id}' was not returned by the workspace "
874+
"metadata provider. Reconfigure the project workspace or retry after "
875+
"workspace metadata recovers."
876+
)
877+
return workspace
878+
848879
return None
849880

850881

tests/mcp/test_project_context.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,83 @@ async def fake_get_active_project(client, project_name, context=None, headers=No
16401640
assert permalink_context.workspace_slug == "team-paul"
16411641
assert permalink_context.workspace_type == "organization"
16421642

1643+
@pytest.mark.asyncio
1644+
async def test_cloud_project_workspace_id_uses_workspace_provider_for_permalink_context(
1645+
self, config_manager, monkeypatch
1646+
):
1647+
"""Injected workspace metadata supplies slug/type without project index discovery."""
1648+
from contextlib import asynccontextmanager
1649+
1650+
import basic_memory.mcp.project_context as project_context
1651+
from basic_memory.mcp.project_context import get_project_client
1652+
from basic_memory.config import ProjectEntry, ProjectMode
1653+
from basic_memory.schemas.project_info import ProjectItem
1654+
from basic_memory.workspace_context import (
1655+
WorkspacePermalinkContext,
1656+
current_workspace_permalink_context,
1657+
)
1658+
1659+
config = config_manager.load_config()
1660+
config.projects["cloud-proj"] = ProjectEntry(
1661+
path=str(config_manager.config_dir.parent / "cloud-proj"),
1662+
mode=ProjectMode.CLOUD,
1663+
workspace_id="per-project-tenant-id",
1664+
)
1665+
config.cloud_api_key = "bmc_test123"
1666+
config_manager.save_config(config)
1667+
1668+
workspace = _workspace(
1669+
tenant_id="per-project-tenant-id",
1670+
workspace_type="organization",
1671+
slug="team-paul",
1672+
name="Team Paul",
1673+
role="editor",
1674+
)
1675+
1676+
async def fake_workspace_provider():
1677+
return [workspace]
1678+
1679+
async def fail_ensure_workspace_project_index(context=None): # pragma: no cover
1680+
raise AssertionError("Configured workspace_id should not require project discovery")
1681+
1682+
monkeypatch.setattr(project_context, "_workspace_provider", fake_workspace_provider)
1683+
monkeypatch.setattr(
1684+
project_context,
1685+
"_ensure_workspace_project_index",
1686+
fail_ensure_workspace_project_index,
1687+
)
1688+
1689+
seen: dict[str, object] = {}
1690+
1691+
@asynccontextmanager
1692+
async def fake_get_client(project_name=None, workspace=None):
1693+
seen["project_name"] = project_name
1694+
seen["workspace"] = workspace
1695+
seen["permalink_context"] = current_workspace_permalink_context()
1696+
yield object()
1697+
1698+
async def fake_get_active_project(client, project_name, context=None, headers=None):
1699+
return ProjectItem(
1700+
id=1,
1701+
external_id="cloud-project-id",
1702+
name=project_name,
1703+
path="/cloud-proj",
1704+
is_default=False,
1705+
)
1706+
1707+
monkeypatch.setattr("basic_memory.mcp.async_client.get_client", fake_get_client)
1708+
monkeypatch.setattr(project_context, "get_active_project", fake_get_active_project)
1709+
1710+
async with get_project_client(project="cloud-proj") as (_client, active_project):
1711+
assert active_project.external_id == "cloud-project-id"
1712+
1713+
assert seen["project_name"] == "cloud-proj"
1714+
assert seen["workspace"] == "per-project-tenant-id"
1715+
permalink_context = cast(WorkspacePermalinkContext | None, seen["permalink_context"])
1716+
assert permalink_context is not None
1717+
assert permalink_context.workspace_slug == "team-paul"
1718+
assert permalink_context.workspace_type == "organization"
1719+
16431720
@pytest.mark.asyncio
16441721
async def test_cloud_project_workspace_id_routes_without_discovery(
16451722
self, config_manager, monkeypatch

0 commit comments

Comments
 (0)