Skip to content

Commit e8e122a

Browse files
committed
fix workspace qualified write permalinks
1 parent 5a34a42 commit e8e122a

2 files changed

Lines changed: 69 additions & 8 deletions

File tree

src/basic_memory/mcp/tools/write_note.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
from basic_memory.mcp.server import mcp
1313
from fastmcp import Context
1414
from basic_memory.schemas.base import Entity
15-
from basic_memory.utils import coerce_dict, parse_tags, validate_project_path
15+
from basic_memory.utils import (
16+
build_qualified_permalink_reference,
17+
coerce_dict,
18+
parse_tags,
19+
validate_project_path,
20+
)
21+
from basic_memory.workspace_context import current_workspace_permalink_context
1622

1723
# Define TagType as a Union that can accept either a string or a list of strings or None
1824
TagType = Union[List[str], str, None]
@@ -274,11 +280,20 @@ async def write_note(
274280
else:
275281
# Re-raise if it's not a conflict error
276282
raise # pragma: no cover
283+
response_permalink = result.permalink
284+
workspace_context = current_workspace_permalink_context()
285+
if response_permalink and workspace_context is not None:
286+
response_permalink = build_qualified_permalink_reference(
287+
active_project.permalink,
288+
response_permalink,
289+
workspace_permalink=workspace_context.workspace_slug,
290+
)
291+
277292
summary = [
278293
f"# {action} note",
279294
f"project: {active_project.name}",
280295
f"file_path: {result.file_path}",
281-
f"permalink: {result.permalink}",
296+
f"permalink: {response_permalink}",
282297
f"checksum: {result.checksum[:8] if result.checksum else 'unknown'}",
283298
]
284299

@@ -315,12 +330,12 @@ async def write_note(
315330

316331
# Log the response with structured data
317332
logger.info(
318-
f"MCP tool response: tool=write_note project={active_project.name} action={action} permalink={result.permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved}"
333+
f"MCP tool response: tool=write_note project={active_project.name} action={action} permalink={response_permalink} observations_count={len(result.observations)} relations_count={len(result.relations)} resolved_relations={resolved} unresolved_relations={unresolved}"
319334
)
320335
if output_format == "json":
321336
return {
322337
"title": result.title,
323-
"permalink": result.permalink,
338+
"permalink": response_permalink,
324339
"file_path": result.file_path,
325340
"checksum": result.checksum,
326341
"action": action.lower(),

test-int/mcp/test_workspace_permalink_integration.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ def team_workspace() -> WorkspaceInfo:
7070
@pytest.fixture
7171
def route_workspaces(app):
7272
@contextmanager
73-
def route(*workspaces: WorkspaceInfo):
74-
with _workspace_routing(app, workspaces):
73+
def route(*workspaces: WorkspaceInfo, forward_permalink_headers: bool = True):
74+
with _workspace_routing(
75+
app,
76+
workspaces,
77+
forward_permalink_headers=forward_permalink_headers,
78+
):
7579
yield
7680

7781
return route
@@ -117,7 +121,12 @@ def _save_permalink_config(
117121

118122

119123
@contextmanager
120-
def _workspace_routing(app, workspaces: Iterable[WorkspaceInfo]):
124+
def _workspace_routing(
125+
app,
126+
workspaces: Iterable[WorkspaceInfo],
127+
*,
128+
forward_permalink_headers: bool = True,
129+
):
121130
"""Route MCP tool HTTP calls through an ASGI-backed cloud workspace seam."""
122131
workspace_list = tuple(workspaces)
123132
workspace_ids = {workspace.tenant_id for workspace in workspace_list}
@@ -128,10 +137,11 @@ async def workspace_provider():
128137
@asynccontextmanager
129138
async def factory(workspace: str | None = None):
130139
assert workspace is None or workspace in workspace_ids
140+
headers = workspace_permalink_headers() if forward_permalink_headers else {}
131141
async with HttpxAsyncClient(
132142
transport=ASGITransport(app=app),
133143
base_url="http://test",
134-
headers=workspace_permalink_headers(),
144+
headers=headers,
135145
) as inner:
136146
yield inner
137147

@@ -461,3 +471,39 @@ async def test_team_workspace_permalink_routes_to_specific_workspace(
461471

462472
workspace_read = await _read_json(mcp_server, identifier=expected_permalink)
463473
assert workspace_read["permalink"] == expected_permalink
474+
475+
476+
@pytest.mark.asyncio
477+
async def test_write_note_by_project_id_qualifies_permalink_when_headers_not_forwarded(
478+
mcp_server,
479+
test_project,
480+
app_config,
481+
team_workspace,
482+
route_workspaces,
483+
):
484+
"""MCP writes should return self-routing IDs even if the API omits slug headers."""
485+
_save_permalink_config(app_config, include_project=True, default_project=None)
486+
487+
title = "Project Id Workspace Permalink"
488+
short_permalink = "permalink-suite/project-id-workspace-permalink"
489+
expected_permalink = f"{team_workspace.slug}/{test_project.name}/{short_permalink}"
490+
491+
with route_workspaces(team_workspace, forward_permalink_headers=False):
492+
write_payload = await _call_json(
493+
mcp_server,
494+
"write_note",
495+
{
496+
"project_id": test_project.external_id,
497+
"title": title,
498+
"directory": "permalink-suite",
499+
"content": f"# {title}\n\nProject ID workspace body.",
500+
"output_format": "json",
501+
},
502+
)
503+
assert write_payload["permalink"] == expected_permalink
504+
505+
workspace_read = await _read_json(
506+
mcp_server,
507+
identifier=f"memory://{write_payload['permalink']}",
508+
)
509+
assert workspace_read["title"] == title

0 commit comments

Comments
 (0)