Skip to content

Commit a961aa2

Browse files
committed
fix(mcp): preserve workspace paths in build context
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent e871298 commit a961aa2

2 files changed

Lines changed: 87 additions & 15 deletions

File tree

src/basic_memory/mcp/project_context.py

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,42 @@ def _canonical_memory_path_for_workspace(
446446
return f"{prefix}/{normalized_remainder}"
447447

448448

449+
def _canonical_memory_path_for_active_route(
450+
active_project: ProjectItem,
451+
path: str,
452+
*,
453+
include_project: bool,
454+
cached_workspace: WorkspaceInfo | None = None,
455+
) -> str:
456+
"""Return the canonical permalink path for the currently routed project/workspace."""
457+
workspace_context = current_workspace_permalink_context()
458+
if workspace_context is not None:
459+
return _canonical_memory_path_for_workspace(
460+
workspace_slug=workspace_context.workspace_slug,
461+
workspace_type=workspace_context.workspace_type,
462+
project_permalink=active_project.permalink,
463+
remainder=path,
464+
include_project=include_project,
465+
)
466+
467+
if cached_workspace is not None:
468+
return _canonical_memory_path_for_workspace(
469+
workspace_slug=cached_workspace.slug,
470+
workspace_type=cached_workspace.workspace_type,
471+
project_permalink=active_project.permalink,
472+
remainder=path,
473+
include_project=include_project,
474+
)
475+
476+
if not include_project:
477+
return path
478+
479+
project_prefix = active_project.permalink
480+
if path == project_prefix or path.startswith(f"{project_prefix}/"):
481+
return path
482+
return f"{project_prefix}/{path}"
483+
484+
449485
def _cloud_workspace_discovery_available(config: BasicMemoryConfig) -> bool:
450486
"""Return True when workspace discovery can be used without forcing local routing."""
451487
from basic_memory.mcp.async_client import (
@@ -1115,8 +1151,11 @@ async def resolve_project_and_path(
11151151
f"Project is constrained to '{resolved_project}', cannot use '{project_prefix}'."
11161152
)
11171153

1118-
resolved_path = (
1119-
f"{cached_project.permalink}/{remainder}" if include_project else remainder
1154+
resolved_path = _canonical_memory_path_for_active_route(
1155+
cached_project,
1156+
remainder,
1157+
include_project=include_project,
1158+
cached_workspace=cached_workspace,
11201159
)
11211160
return cached_project, resolved_path, True
11221161

@@ -1151,25 +1190,30 @@ async def resolve_project_and_path(
11511190
)
11521191
await _set_cached_active_project(context, active_project)
11531192

1154-
resolved_path = (
1155-
f"{resolved.permalink}/{remainder}" if include_project else remainder
1193+
resolved_path = _canonical_memory_path_for_active_route(
1194+
active_project,
1195+
remainder,
1196+
include_project=include_project,
1197+
cached_workspace=cached_workspace,
11561198
)
11571199
return active_project, resolved_path, True
11581200

11591201
# Trigger: no resolvable project prefix in the memory URL
11601202
# Why: preserve existing memory URL behavior within the active project
11611203
# Outcome: use the active project and normalize the path for lookup
11621204
active_project = await get_active_project(client, project, context, headers)
1163-
resolved_path = normalized_path
1164-
if include_project:
1165-
# Trigger: project-prefixed permalinks are enabled and the path lacks a prefix
1166-
# Why: ensure memory URL lookups align with canonical permalinks
1167-
# Outcome: prefix the path with the active project's permalink
1168-
project_prefix = active_project.permalink
1169-
if resolved_path != project_prefix and not resolved_path.startswith(
1170-
f"{project_prefix}/"
1171-
):
1172-
resolved_path = f"{project_prefix}/{resolved_path}"
1205+
# Trigger: memory URL has no explicit project route segment
1206+
# Why: active cloud workspaces store canonical organization paths as
1207+
# <workspace>/<project>/<path>, while personal/local projects may use
1208+
# only <project>/<path> depending on config.
1209+
# Outcome: normalize against the already-selected route instead of
1210+
# prepending only the project slug and losing workspace context.
1211+
resolved_path = _canonical_memory_path_for_active_route(
1212+
active_project,
1213+
normalized_path,
1214+
include_project=include_project,
1215+
cached_workspace=cached_workspace,
1216+
)
11731217
return active_project, resolved_path, True
11741218

11751219

tests/mcp/test_tool_build_context.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mcp.server.fastmcp.exceptions import ToolError
66

7-
from basic_memory.mcp.tools import build_context
7+
from basic_memory.mcp.tools import build_context, write_note
88

99

1010
@pytest.mark.asyncio
@@ -75,6 +75,34 @@ async def test_get_discussion_context_pattern(client, test_graph, test_project):
7575
assert result["metadata"]["depth"] == 1
7676

7777

78+
@pytest.mark.asyncio
79+
async def test_build_context_project_id_preserves_workspace_canonical_path(app, test_project):
80+
"""project_id routing keeps organization workspace prefixes in memory URL lookups."""
81+
from basic_memory.workspace_context import workspace_permalink_context
82+
83+
with workspace_permalink_context(workspace_slug="team-paul", workspace_type="organization"):
84+
await write_note(
85+
project_id=test_project.external_id,
86+
title="Workspace Build Context Note",
87+
directory="tests",
88+
content="Build context should find this workspace note",
89+
)
90+
91+
result = await build_context(
92+
project_id=test_project.external_id,
93+
url="memory://tests/*",
94+
timeframe="30d",
95+
)
96+
97+
assert isinstance(result, dict)
98+
assert len(result["results"]) == 1
99+
primary = result["results"][0]["primary_result"]
100+
assert primary["permalink"] == (
101+
f"team-paul/{test_project.name}/tests/workspace-build-context-note"
102+
)
103+
assert primary["content"] == "Build context should find this workspace note"
104+
105+
78106
@pytest.mark.asyncio
79107
async def test_get_discussion_context_timeframe(client, test_graph, test_project):
80108
"""Test timeframe parameter filtering."""

0 commit comments

Comments
 (0)