From ac6ba560c17bae74b41a832da1b863bd07e3f93b Mon Sep 17 00:00:00 2001 From: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:07:05 -0700 Subject: [PATCH 1/2] fix(mcp): script mode without install, recall runs in the server's cwd 1. `python src/sqlite_graph_memory/mcp_server.py` crashed at import with ModuleNotFoundError: Python puts the script's own dir, not src/, on sys.path. Loaded as a package the version is imported as before; run as a script it is read from the sibling __init__.py. sys.path is not touched. 2. run_recall spawned brain_ask with cwd=ROOT_DIR. That dates from 975659b, when ROOT_DIR was the repo root holding ./index; since the move into the package it is site-packages/sqlite_graph_memory, so relative BRAIN_INDEX_DIR / TURNSTATE_DB missed the user's files. The child now inherits the server's cwd. Tests in tests/test_mcp_script_mode.py, both seen red on the old code. Assisted-by: Claude Code / claude-opus-5-5 Machine: MacBook-Anton Account: dzyatkovskiy.a2@gmail.com Operator: anton Co-Authored-By: Claude Opus 5.5 --- src/sqlite_graph_memory/mcp_server.py | 28 +++++++++++-- tests/test_mcp_script_mode.py | 59 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 tests/test_mcp_script_mode.py diff --git a/src/sqlite_graph_memory/mcp_server.py b/src/sqlite_graph_memory/mcp_server.py index 49708f5..e512486 100644 --- a/src/sqlite_graph_memory/mcp_server.py +++ b/src/sqlite_graph_memory/mcp_server.py @@ -22,7 +22,26 @@ import sys, os, json, subprocess, tempfile from pathlib import Path -from sqlite_graph_memory import __version__ + +def _package_version(): + """Version of the package this file belongs to. + + Imported normally when loaded as part of the package (installed wheel, + editable install, tests). Run as a plain script + (``python src/sqlite_graph_memory/mcp_server.py``) the package is not on + sys.path, so read the sibling __init__.py instead of touching sys.path. + """ + if __package__: + from sqlite_graph_memory import __version__ as v + return v + import re + init = Path(__file__).resolve().parent / "__init__.py" + m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', + init.read_text(encoding="utf-8"), re.M) + return m.group(1) if m else "0+unknown" + + +__version__ = _package_version() # Ensure UTF-8 for stdio communication on all platforms (including Windows console) try: @@ -32,7 +51,7 @@ except Exception: pass -# Where the recall subprocess runs. The server deliberately shells out instead +# This package's directory. The server deliberately shells out instead # of importing brain_ask: a model load that segfaults or runs out of memory then # kills a child process and returns an MCP error, rather than taking the server # down mid-session. @@ -84,6 +103,10 @@ def run_recall(query: str, mode: str = "associative") -> tuple[str, bool]: An empty answer file means no notes matched; stdout is never promoted to an answer because it carries startup notices, load reports, and tokenizer warnings. + + The child inherits the server's working directory (no cwd=), so relative + BRAIN_INDEX_DIR / TURNSTATE_DB resolve where the user launched the server, + not inside site-packages/sqlite_graph_memory. """ base_cmd = _brain_ask_cmd() if base_cmd is None: @@ -113,7 +136,6 @@ def run_recall(query: str, mode: str = "associative") -> tuple[str, bool]: call_env = {**os.environ, "BRAIN_ANSWER_OUT": str(temp_ans_file)} res = subprocess.run( cmd, - cwd=str(ROOT_DIR), env=call_env, capture_output=True, text=True, diff --git a/tests/test_mcp_script_mode.py b/tests/test_mcp_script_mode.py new file mode 100644 index 0000000..86f4b20 --- /dev/null +++ b/tests/test_mcp_script_mode.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""Script-mode and working-directory behaviour of the MCP server. + +Kept in its own file so it does not collide with edits to test_mcp_server.py. +Runs offline: no model, no index, no network. +""" +import json +import os +import subprocess +import sys +from pathlib import Path + +SRC = Path(__file__).resolve().parents[1] / "src" +sys.path.insert(0, str(SRC)) +from sqlite_graph_memory import __version__ # noqa: E402 +from sqlite_graph_memory import mcp_server # noqa: E402 + +SCRIPT = SRC / "sqlite_graph_memory" / "mcp_server.py" + + +def test_script_mode_starts_without_installed_package(tmp_path): + """`python src/sqlite_graph_memory/mcp_server.py` must not need the package on sys.path. + + Python puts the script's own directory (not src/) on sys.path, so an + absolute `from sqlite_graph_memory import ...` at import time crashed with + ModuleNotFoundError before the server answered anything (issue #24). + `-S` drops site-packages so an installed or editable copy cannot mask it. + """ + env = {k: v for k, v in os.environ.items() if k != "PYTHONPATH"} + req = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}} + res = subprocess.run( + [sys.executable, "-S", str(SCRIPT)], + input=json.dumps(req) + "\n", + capture_output=True, text=True, cwd=str(tmp_path), env=env, timeout=30, + ) + assert res.returncode == 0, res.stderr + resp = json.loads(res.stdout.strip().splitlines()[-1]) + assert resp["result"]["serverInfo"]["version"] == __version__ + + +def test_recall_subprocess_inherits_server_cwd(tmp_path, monkeypatch): + """Relative BRAIN_INDEX_DIR / TURNSTATE_DB must resolve against the user's cwd. + + On an installed wheel the package dir is site-packages/sqlite_graph_memory; + running brain_ask there made `BRAIN_INDEX_DIR=index` miss the user's ./index. + """ + work = tmp_path / "work" + work.mkdir() + stub = tmp_path / "stub_brain_ask.py" + stub.write_text( + "import os, pathlib\n" + "pathlib.Path(os.environ['BRAIN_ANSWER_OUT']).write_text(os.getcwd(), encoding='utf-8')\n", + encoding="utf-8", + ) + monkeypatch.setattr(mcp_server, "_brain_ask_cmd", lambda: [sys.executable, str(stub)]) + monkeypatch.chdir(work) + text, is_error = mcp_server.run_recall("anything") + assert is_error is False, text + assert Path(text).resolve() == work.resolve() From 9809647533fd1472d0e423f0762f6521c51b2aeb Mon Sep 17 00:00:00 2001 From: Anton Dziatkovskii <194927794+tonydzi@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:14:44 -0700 Subject: [PATCH 2/2] ci: run the installed server from the user's directory, as people launch it The tests run from the source tree and the installable job only checked that the commands start. Neither reproduced how the MCP server is actually used: launched from the user's own directory, with a relative BRAIN_INDEX_DIR, against the installed wheel. That is exactly the bug this PR fixes (the recall child ran with cwd inside site-packages), and CI stayed green on it. The new step builds a one-row index in /tmp/userdir and launches sgm-mcp --test from there. Index found => it stops at the missing [embeddings] extra; index not found => "need an embedding index" and the job fails. Red/green shown locally with the same step body: origin/main -> FAIL ("need an embedding index", rc=1); this branch -> PASS (rc=0). Raised independently by three of six models in the retro panel (GLM-5.3, GPT-6 Sol, Grok 4.3). Assisted-by: Claude Code / claude-opus-5-5 Machine: MacBook-Anton Account: dzyatkovskiy.a2@gmail.com Operator: anton Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tests.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d2ab3f..6d48023 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,3 +90,26 @@ jobs: case "$out" in *Traceback*) echo "::error::base install printed a traceback"; exit 1 ;; esac + - name: the installed server must find an index relative to the user's directory + # The tests run from the source tree, and the steps above only check that + # the commands start. Neither reproduces how people actually launch the MCP + # server: from their OWN directory, with a relative BRAIN_INDEX_DIR, against + # the installed wheel. The recall child used to run with cwd set to the + # package directory inside site-packages, so "index" resolved there and the + # user's index was never found. CI stayed green on exactly that bug, so this + # step reproduces it: a minimal index in /tmp/userdir, launch from there. + # Index found => brain_ask gets past loading and stops at the missing + # [embeddings] extra. Index NOT found => "need an embedding index". + run: | + mkdir -p /tmp/userdir/index + cd /tmp/userdir + /tmp/clean/bin/python -c "import numpy as np, pickle; np.save('index/_brain_e5.npy', np.zeros((1, 4), dtype='float32')); pickle.dump([{'path': 'a.md', 'chunk_idx': 0}], open('index/_brain_e5_meta.pkl', 'wb'))" + out=$(env -u PYTHONPATH BRAIN_INDEX_DIR=index /tmp/clean/bin/sgm-mcp --test "agent memory" 2>&1 || true) + echo "$out" + case "$out" in + *"need an embedding index"*) echo "::error::installed server looked for the index outside the user's directory"; exit 1 ;; + esac + case "$out" in + *"sqlite-graph-memory[embeddings]"*) echo "index found from the user's directory" ;; + *) echo "::error::unexpected output from the installed server"; exit 1 ;; + esac