Skip to content

Commit f719ad7

Browse files
phernandezclaude
andcommitted
fix(mcp): reject cross-project memory:// source in move_note
Codex review: the memory:// resolution captured the resolved path but discarded the resolved project, so a 'memory://other-project/...' source would be looked up against the active project's client (misroute). move_note is single-project; reject when the source URL resolves to a different project than the active one, consistent with the cross-project destination guard. Same-project memory:// URLs are unaffected (verified by the existing tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 4017d9a commit f719ad7

1 file changed

Lines changed: 28 additions & 1 deletion

File tree

src/basic_memory/mcp/tools/move_note.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,10 +660,37 @@ async def move_note(
660660
# 404s. resolve_project_and_path strips the prefix and normalizes the path the
661661
# same way the sibling tools do.
662662
# Outcome: move_note resolves memory:// URLs identically to read/edit/delete.
663-
_, resolved_identifier, _ = await resolve_project_and_path(
663+
source_project, resolved_identifier, _ = await resolve_project_and_path(
664664
client, identifier, active_project.name, context
665665
)
666666

667+
# Trigger: a memory:// identifier whose project prefix resolves to a DIFFERENT project
668+
# than the one move_note is operating on (e.g. "memory://other-project/...").
669+
# Why: get_project_client already bound knowledge_client to the active project, so the
670+
# resolve_entity below runs against the active project regardless of the URL's
671+
# project. Honoring the cross-project prefix would misroute (path normalized for
672+
# the other project, looked up in the active one); move_note cannot move across
673+
# projects.
674+
# Outcome: reject up front with the cross-project guidance instead of misrouting.
675+
if source_project.external_id != active_project.external_id:
676+
logger.info(
677+
f"Move rejected: source '{identifier}' resolves to project "
678+
f"'{source_project.name}', not the active project '{active_project.name}'"
679+
)
680+
if output_format == "json":
681+
return {
682+
"moved": False,
683+
"title": None,
684+
"permalink": None,
685+
"file_path": None,
686+
"source": identifier,
687+
"destination": destination_path,
688+
"error": "CROSS_PROJECT_MOVE_NOT_SUPPORTED",
689+
}
690+
return _format_cross_project_error_response(
691+
identifier, destination_path, active_project.name, source_project.name
692+
)
693+
667694
# Resolve once and reuse the entity ID across extension validation and move.
668695
source_ext = "md" # Default to .md if we can't determine source extension
669696
resolved_entity_id: str | None = None

0 commit comments

Comments
 (0)