Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 11 additions & 8 deletions mempalace/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,19 @@ def cmd_mine(args):


def cmd_search(args):
from .searcher import search
from .searcher import search, SearchError

palace_path = os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
search(
query=args.query,
palace_path=palace_path,
wing=args.wing,
room=args.room,
n_results=args.results,
)
try:
search(
query=args.query,
palace_path=palace_path,
wing=args.wing,
room=args.room,
n_results=args.results,
)
except SearchError:
sys.exit(1)


def cmd_wakeup(args):
Expand Down
11 changes: 7 additions & 4 deletions mempalace/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def _get_collection(create=False):
def _no_palace():
return {
"error": "No palace found",
"palace_path": _config.palace_path,
"hint": "Run: mempalace init <dir> && mempalace mine <dir>",
}

Expand Down Expand Up @@ -744,9 +743,13 @@ def handle_request(request):
"id": req_id,
"result": {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]},
}
except Exception as e:
logger.error(f"Tool error in {tool_name}: {e}")
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32000, "message": str(e)}}
except Exception:
logger.exception(f"Tool error in {tool_name}")
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {"code": -32000, "message": "Internal tool error"},
}

return {
"jsonrpc": "2.0",
Expand Down
18 changes: 14 additions & 4 deletions mempalace/searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
Returns verbatim text — the actual words, never summaries.
"""

import sys
import logging
from pathlib import Path

import chromadb

logger = logging.getLogger("mempalace_mcp")


class SearchError(Exception):
"""Raised when search cannot proceed (e.g. no palace found)."""


def search(query: str, palace_path: str, wing: str = None, room: str = None, n_results: int = 5):
"""
Expand All @@ -23,7 +29,7 @@ def search(query: str, palace_path: str, wing: str = None, room: str = None, n_r
except Exception:
print(f"\n No palace found at {palace_path}")
print(" Run: mempalace init <dir> then mempalace mine <dir>")
sys.exit(1)
raise SearchError(f"No palace found at {palace_path}")

# Build where filter
where = {}
Expand All @@ -47,7 +53,7 @@ def search(query: str, palace_path: str, wing: str = None, room: str = None, n_r

except Exception as e:
print(f"\n Search error: {e}")
sys.exit(1)
raise SearchError(f"Search error: {e}") from e

docs = results["documents"][0]
metas = results["metadatas"][0]
Expand Down Expand Up @@ -95,7 +101,11 @@ def search_memories(
client = chromadb.PersistentClient(path=palace_path)
col = client.get_collection("mempalace_drawers")
except Exception as e:
return {"error": f"No palace found at {palace_path}: {e}"}
logger.error("No palace found at %s: %s", palace_path, e)
return {
"error": "No palace found",
"hint": "Run: mempalace init <dir> && mempalace mine <dir>",
}

# Build where filter
where = {}
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@


@pytest.fixture(scope="session", autouse=True)
def _isolate_home(tmp_path_factory):
def _isolate_home():
"""Ensure HOME points to a temp dir for the entire test session.

The env vars were already set at module level (above) so that
Expand Down
6 changes: 3 additions & 3 deletions tests/test_dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ def test_stats(self):
original = "We decided to use GraphQL instead of REST. " * 10
compressed = d.compress(original)
stats = d.compression_stats(original, compressed)
assert stats["ratio"] > 1
assert stats["original_chars"] > stats["compressed_chars"]
assert stats["size_ratio"] > 1
assert stats["original_chars"] > stats["summary_chars"]

def test_count_tokens(self):
assert Dialect.count_tokens("hello world") == len("hello world") // 3
assert Dialect.count_tokens("hello world") == 2


class TestZettelEncoding:
Expand Down
4 changes: 4 additions & 0 deletions tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ def _patch_mcp_server(monkeypatch, config, palace_path, kg):
"""Patch the mcp_server module globals to use test fixtures."""
from mempalace import mcp_server

assert getattr(config, "palace_path", None) == palace_path, (
f"config.palace_path ({getattr(config, 'palace_path', None)!r}) does not match palace_path fixture ({palace_path!r})"
)
monkeypatch.setattr(mcp_server, "_config", config)
monkeypatch.setattr(mcp_server, "_kg", kg)

Expand Down Expand Up @@ -149,6 +152,7 @@ def test_get_taxonomy(self, monkeypatch, config, palace_path, seeded_collection,
assert result["taxonomy"]["notes"]["planning"] == 1

def test_no_palace_returns_error(self, monkeypatch, config, kg):
config._file_config["palace_path"] = "/nonexistent/path"
_patch_mcp_server(monkeypatch, config, "/nonexistent/path", kg)
from mempalace.mcp_server import tool_status

Expand Down
10 changes: 10 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.