Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
47736ba
feat(mcp): route project-qualified paths in the POSIX tools
phernandez Aug 31, 2026
72caf40
fix(mcp): route and refuse from one addressable-project set
phernandez Sep 1, 2026
30ac59b
fix(mcp): let an advertised mount claim its first path segment
phernandez Sep 1, 2026
99d6d8c
fix(mcp): bind posix mounts to their workspace and project permalink
phernandez Sep 1, 2026
cf8eea2
fix(mcp): route only explicitly workspace-qualified posix paths
phernandez Sep 1, 2026
d862000
fix(mcp): make project identity and returned paths one rule each
phernandez Sep 1, 2026
b61e391
fix(mcp): requalify by position, and stop inferring workspaces from s…
phernandez Sep 1, 2026
8214e04
fix(mcp): decide route versus path by precedence, not by parsing
phernandez Sep 1, 2026
1f6194f
fix(mcp): let a qualified name reach the workspace it names
phernandez Sep 1, 2026
8fcac42
fix(core): make every advertised mount addressable, root included
phernandez Sep 2, 2026
19662cb
fix(mcp): route-versus-path protects exactly one project, not none
phernandez Sep 2, 2026
6f1bd64
fix(core): reject project permalinks with any empty segment
phernandez Sep 2, 2026
f66b3b7
fix(mcp): strip an explicit project's own prefix without discovery
phernandez Sep 2, 2026
195c48b
fix(mcp): keep emitted workspace routes replayable with one mount
phernandez Sep 2, 2026
9ba36c5
fix(core): refuse two projects sharing one mount permalink
phernandez Sep 2, 2026
52754f9
fix(mcp): give the workspace probe the authoritative slug precedence
phernandez Sep 2, 2026
1303e8a
fix(mcp): decide route agreement by identity, not by counting slashes
phernandez Sep 2, 2026
24024d9
fix(mcp): scope the duplicate-mount error to the paths that name it
phernandez Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions src/basic_memory/cli/commands/posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
_validate_output_flags,
console,
)

from basic_memory.schemas.directory import DEFAULT_DIRECTORY_PAGE_SIZE

# MCP tool functions are imported inside each command: importing
Expand Down Expand Up @@ -358,10 +359,16 @@ def _add_tree_branches(branch: Tree, entries: dict[str, _TreeEntry]) -> None:
_add_tree_branches(child, entry.children)


def _display_tree(result: dict[str, Any], path: str) -> None:
"""Render find results as a Rich tree rooted at the search path."""
entries = _build_tree(list(result.get("nodes", [])), path)
tree = Tree(f"[bold cyan]{markup_escape(path)}[/bold cyan]")
def _display_tree(result: dict[str, Any], label: str, root: str) -> None:
"""Render find results as a Rich tree rooted at the search path.

``label`` is the caller's spelling of the root; ``root`` is the routed
project-relative path the node paths actually start with — a qualified
'<project>/dir' input strips its project prefix in the shared tool layer,
so the two differ exactly when the input carried a project prefix (#1415).
"""
entries = _build_tree(list(result.get("nodes", [])), root)
tree = Tree(f"[bold cyan]{markup_escape(label)}[/bold cyan]")
if not entries:
tree.add("[dim]empty[/dim]")
_add_tree_branches(tree, entries)
Expand All @@ -379,10 +386,14 @@ def _print_plain_tree_level(entries: dict[str, _TreeEntry], depth: int) -> None:
_print_plain_tree_level(entry.children, depth + 1)


def _plain_tree(result: dict[str, Any], path: str) -> None:
"""Render the tree as two-space-indented lines; pagination note on stderr."""
print(path)
_print_plain_tree_level(_build_tree(list(result.get("nodes", [])), path), depth=1)
def _plain_tree(result: dict[str, Any], label: str, root: str) -> None:
"""Render the tree as two-space-indented lines; pagination note on stderr.

``label``/``root`` split as in ``_display_tree``: print the caller's
spelling, strip the routed project-relative root from node paths.
"""
print(label)
_print_plain_tree_level(_build_tree(list(result.get("nodes", [])), root), depth=1)
if result.get("has_more") is True:
print(f"… more entries (page {result.get('page', 1)}; use --page)", file=sys.stderr)

Expand Down Expand Up @@ -811,16 +822,20 @@ def tree(
# tree is a client-side recombination of find: the same flat listing, with
# the hierarchy rebuilt from directory paths for display, so its JSON
# contract is exactly find's payload.
from basic_memory.mcp.tools import find as mcp_find
from basic_memory.mcp.tools.posix_tools import find_listing
from fastmcp.exceptions import ToolError

try:
validate_routing_flags(local, cloud)
_validate_output_flags(json_output, plain)

# find returns the listing and the root its node paths are relative to
# from one resolution. Resolving here as well cost a second project-list
# round trip on every cloud call, because a CLI invocation has no
# FastMCP context for the per-request cache to live in (#1421).
with force_routing(local=local, cloud=cloud):
result = run_with_cleanup(
mcp_find(
result, root = run_with_cleanup(
find_listing(
path,
name=name,
depth=depth,
Expand All @@ -834,9 +849,9 @@ def tree(
if mode == "json":
_print_json(result)
elif mode == "plain":
_plain_tree(result, path)
_plain_tree(result, path, root)
else:
_display_tree(result, path)
_display_tree(result, path, root)
except (ValueError, ToolError) as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1)
Expand Down
Loading
Loading