From acdab2ecfc64bedcc1e8a5d80b9e9c1f5f3c48e6 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 02:30:13 +0000 Subject: [PATCH] fix(mcp): apply workspace-canonical prefix in resolve_project_and_path fallback When build_context is called with project_id for a project in an organization workspace (e.g. "big-team/main"), the fallback path in resolve_project_and_path was only prepending the project permalink ("main/tests/*") and ignoring the workspace slug. Notes in organization workspaces are stored with the full workspace-prefixed permalink ("big-team/main/tests/test"), so the GLOB query never matched. Fix by calling _canonical_memory_path_for_workspace in the fallback when a workspace context is active. This produces the correct prefix for both organization workspaces ("big-team/main/tests/*") and personal workspaces ("main/tests/*"), consistent with how the existing workspace_context and cached_project paths already handle URLs that begin with the qualified prefix. Add tests for: - build_context skips URL detection when project_id is provided - build_context correctly resolves workspace-prefixed paths in org workspaces Closes #799 Co-authored-by: Paul Hernandez Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- src/basic_memory/mcp/project_context.py | 28 ++++++++--- tests/mcp/test_tool_build_context.py | 63 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/src/basic_memory/mcp/project_context.py b/src/basic_memory/mcp/project_context.py index 826725d7..aa3df5a0 100644 --- a/src/basic_memory/mcp/project_context.py +++ b/src/basic_memory/mcp/project_context.py @@ -1163,13 +1163,27 @@ async def resolve_project_and_path( resolved_path = normalized_path if include_project: # Trigger: project-prefixed permalinks are enabled and the path lacks a prefix - # Why: ensure memory URL lookups align with canonical permalinks - # Outcome: prefix the path with the active project's permalink - project_prefix = active_project.permalink - if resolved_path != project_prefix and not resolved_path.startswith( - f"{project_prefix}/" - ): - resolved_path = f"{project_prefix}/{resolved_path}" + # Why: ensure memory URL lookups align with canonical permalinks for both + # local and workspace-routed projects; organization workspaces use a + # workspace-slug prefix (e.g. "big-team/main/tests/*") while personal + # workspaces and pure-local projects use just the project permalink + # Outcome: prefix the path using the workspace-canonical form when a + # workspace context is active; fall back to project-only prefix otherwise + fallback_workspace_ctx = current_workspace_permalink_context() + if fallback_workspace_ctx: + resolved_path = _canonical_memory_path_for_workspace( + workspace_slug=fallback_workspace_ctx.workspace_slug, + workspace_type=fallback_workspace_ctx.workspace_type, + project_permalink=active_project.permalink, + remainder=normalized_path, + include_project=True, + ) + else: + project_prefix = active_project.permalink + if resolved_path != project_prefix and not resolved_path.startswith( + f"{project_prefix}/" + ): + resolved_path = f"{project_prefix}/{resolved_path}" return active_project, resolved_path, True diff --git a/tests/mcp/test_tool_build_context.py b/tests/mcp/test_tool_build_context.py index a0bccdc0..49b98c17 100644 --- a/tests/mcp/test_tool_build_context.py +++ b/tests/mcp/test_tool_build_context.py @@ -5,6 +5,7 @@ import pytest from mcp.server.fastmcp.exceptions import ToolError from basic_memory.mcp.tools import build_context +from basic_memory.mcp.tools.write_note import write_note @pytest.mark.asyncio @@ -224,3 +225,65 @@ async def test_build_context_markdown_not_found(client, test_project): assert isinstance(result, str) assert "No results found" in result assert test_project.name in result + + +@pytest.mark.asyncio +async def test_build_context_skips_url_detection_when_project_id_provided( + monkeypatch, app, test_project, test_graph +): + """project_id is authoritative, so memory URL discovery must not run first.""" + + async def fail_if_called(*args, **kwargs): + raise AssertionError("project_id routing should bypass URL discovery") + + import importlib + + bc_module = importlib.import_module("basic_memory.mcp.tools.build_context") + monkeypatch.setattr(bc_module, "detect_project_from_memory_url_prefix", fail_if_called) + + result = await build_context( + url=f"memory://{test_project.name}/test/root", + project_id=test_project.external_id, + ) + + assert isinstance(result, dict) + + +@pytest.mark.asyncio +async def test_build_context_project_id_org_workspace_routing(app, test_project): + """build_context must resolve the correct workspace-prefixed path when project_id is used + inside an organization workspace context. + + Before the fix, the fallback in resolve_project_and_path only prepended the project + permalink (e.g. "test-project/tests/*") and ignored the workspace slug, so notes stored + as "team-paul/test-project/tests/" were never found. + """ + from basic_memory.workspace_context import workspace_permalink_context + + workspace_slug = "team-paul" + + # Write a note inside the org workspace context so its permalink is + # "team-paul/test-project/workspace-tests/team-note". + with workspace_permalink_context(workspace_slug, "organization"): + await write_note( + project=test_project.name, + title="Team Note", + directory="workspace-tests", + content="# Team Note\n\nContent written in org workspace context.", + ) + + # build_context with project_id inside the same workspace context should find the note. + with workspace_permalink_context(workspace_slug, "organization"): + result = await build_context( + url="memory://workspace-tests/*", + project_id=test_project.external_id, + ) + + assert isinstance(result, dict) + assert result["metadata"]["primary_count"] >= 1, ( + "build_context should find the note written under the org workspace context" + ) + permalinks = [r["primary_result"]["permalink"] for r in result["results"]] + assert any("team-paul" in p for p in permalinks), ( + f"Expected workspace-prefixed permalink; got {permalinks}" + )