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
106 changes: 106 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Examples & Agent Integrations

This directory contains reference integrations wiring `sqlite-graph-memory` into different AI agent harnesses.

---

## Available Examples

| File | Harness / Protocol | Description |
| :--- | :--- | :--- |
| [`mcp_server.py`](mcp_server.py) | **MCP (Model Context Protocol)** | Stdio MCP server exposing memory recall as an agent tool. Works with Cursor, Claude Desktop, Antigravity, Zed, and Windsurf. |
| [`mcp-config.json`](mcp-config.json) | **MCP Client Config** | Sample configuration block for `claude_desktop_config.json` or Cursor MCP settings. |
| [`claude-code-stop-hook.json`](claude-code-stop-hook.json) | **Claude Code Stop Hook** | Configuration snippet registering `turnstate_hook.py` as an automatic post-turn ledger hook. |

---

## 1. MCP Server Integration (`mcp_server.py`)

The MCP server exposes a single tool (`memory_recall`) that provides associative memory to any MCP-compliant agent.

### Prerequisites

1. Install requirements:
```bash
pip install -r requirements.txt
```
2. Build the embedding index over your markdown notes:
```bash
python index_notes.py /path/to/your/notes
```

### Tool Definition

- **Tool Name:** `memory_recall`
- **Arguments:**
- `query` (string, required): The search question or topic.
- `mode` (string, optional, default: `"associative"`):
- `"associative"`: Vector search + 1-hop `[[wikilink]]` graph expansion + cross-encoder reranking.
- `"direct"`: Vector search + cross-encoder reranking (no graph expansion).
- `"ab"`: Runs both pipelines, logs comparative telemetry to `turnstate.db`, and returns the comparison.

### MCP Client Setup

Add the server to your MCP client config (e.g. `claude_desktop_config.json` or Cursor `Settings > MCP`):

```json
{
"mcpServers": {
"sqlite-graph-memory": {
"command": "python",
"args": [
"/absolute/path/to/sqlite-graph-memory/examples/mcp_server.py"
],
"env": {
"BRAIN_INDEX_DIR": "/absolute/path/to/sqlite-graph-memory/index",
"TURNSTATE_DB": "/absolute/path/to/sqlite-graph-memory/turnstate.db"
}
}
}
}
```

### Direct CLI Verification

You can verify the MCP server directly from the command line without launching an MCP client:

```bash
# Test default associative recall
python examples/mcp_server.py --test "how do I think about agent memory"

# Test direct (vector-only) recall
python examples/mcp_server.py --test "how do I think about agent memory" --direct

# Test A/B recall mode
python examples/mcp_server.py --test "how do I think about agent memory" --ab
```

---

## 2. Claude Code Stop Hook (`claude-code-stop-hook.json`)

Registers `turnstate_hook.py` to run after every assistant turn in Claude Code. It extracts session context (asks, summaries, files touched, tools used, commands run, decisions made) and appends a row to `turnstate.db` at **0 token cost**.

To register, add the snippet from [`claude-code-stop-hook.json`](claude-code-stop-hook.json) to `~/.claude/settings.json`.

Inspect the recorded turns anytime:
```bash
python turnstate_show.py --stats
python turnstate_show.py --n 10
```

---

## Environment Variables Reference

All components share standard configuration variables:

| Variable | Default | Purpose |
| :--- | :--- | :--- |
| `BRAIN_INDEX_DIR` | `./index` | Directory where `index_notes.py` writes and `brain_ask.py` reads `_brain_e5.npy` and metadata. |
| `TURNSTATE_DB` | `./turnstate.db` | SQLite database holding per-turn ledger (`turns`) and A/B telemetry (`ab_recall`). |
| `BRAIN_ANSWER_OUT` | `./_brain_answer.txt` | File where the formatted context bundle or answer is mirrored. |
| `BRAIN_INDEX_HIDDEN` | `0` | If `1`, indexes hidden dot-directories too (by default skipped). |
| `BRAIN_AB_JSON` | `None` | If set, `--ab` mode writes a structured JSON diff to this file path. |
| `AB_SOURCE` | `work` | Telemetry label written to `ab_recall.source` (`work`, `eval`, etc.). |

15 changes: 15 additions & 0 deletions examples/mcp-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"mcpServers": {
"sqlite-graph-memory": {
"command": "python",
"args": [
"/path/to/sqlite-graph-memory/examples/mcp_server.py"
],
"env": {
"BRAIN_INDEX_DIR": "/path/to/sqlite-graph-memory/index",
"TURNSTATE_DB": "/path/to/sqlite-graph-memory/turnstate.db"
}
}
}
}

252 changes: 252 additions & 0 deletions examples/mcp_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
"""mcp_server.py — Model Context Protocol (MCP) server for sqlite-graph-memory.

Exposes associative memory recall as a standard MCP tool over stdio transport.
Compatible with any MCP client (Cursor, Claude Desktop, Antigravity, Zed, Windsurf).

Protocol: JSON-RPC 2.0 over stdio (newline-delimited JSON).
Zero third-party dependencies required (pure standard library).

USAGE:
# Normal stdio mode (started by MCP client)
python examples/mcp_server.py

# Direct test mode (runs a recall query without an MCP client)
python examples/mcp_server.py --test "how do I think about agent memory"

Config (env, optional):
BRAIN_INDEX_DIR dir holding embedding index (default: ./index)
TURNSTATE_DB SQLite db for telemetry (default: ./turnstate.db)
BRAIN_ANSWER_OUT file to mirror answer into (default: ./_brain_answer.txt)
"""
import sys, os, json, subprocess, tempfile
from pathlib import Path

# Ensure UTF-8 for stdio communication on all platforms (including Windows console)
try:
sys.stdin.reconfigure(encoding='utf-8')
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except Exception:
pass

ROOT_DIR = Path(__file__).resolve().parents[1]
BRAIN_ASK_SCRIPT = ROOT_DIR / "brain_ask.py"

SERVER_INFO = {
"name": "sqlite-graph-memory",
"version": "0.1.3"
}

TOOLS = [
{
"name": "memory_recall",
"description": (
"Associative memory recall over linked markdown notes. "
"Retrieves relevant context using vector search, 1-hop [[wikilink]] graph expansion, "
"and cross-encoder reranking. Returns a formatted context bundle."
),
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query or topic to retrieve memory for."
},
"mode": {
"type": "string",
"enum": ["associative", "direct", "ab"],
"default": "associative",
"description": (
"Retrieval mode: 'associative' (vector + graph expansion, recommended), "
"'direct' (vector search only), or 'ab' (run both and log comparison telemetry)."
)
}
},
"required": ["query"]
}
}
]


def run_recall(query: str, mode: str = "associative") -> str:
"""Invoke brain_ask.py in a crash-safe subprocess and return the retrieved text."""
if not BRAIN_ASK_SCRIPT.exists():
return f"Error: brain_ask.py not found at {BRAIN_ASK_SCRIPT}"

if mode not in ("associative", "direct", "ab"):
mode = "associative"

cmd = [sys.executable, str(BRAIN_ASK_SCRIPT)]
if mode == "ab":
cmd.append("--ab")
elif mode == "direct":
cmd.append("--ask")
else: # associative (default)
cmd.extend(["--graph", "--ask"])
cmd.append(query)

with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tf:
temp_ans_file = Path(tf.name)

try:
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,
encoding="utf-8",
errors="replace",
timeout=60
)
if res.returncode != 0:
err = (res.stderr or res.stdout or "").strip()
return f"Recall error (exit code {res.returncode}):\n{err}"

# If --ask mode was used, read the scoped per-call answer file
if temp_ans_file.exists():
try:
content = temp_ans_file.read_text(encoding="utf-8", errors="replace").strip()
if content:
return content
except Exception:
pass

return res.stdout.strip() or "(no matching notes found)"
except subprocess.TimeoutExpired:
return "Recall error: timed out after 60 seconds."
except Exception as e:
return f"Recall exception: {e}"
finally:
if temp_ans_file.exists():
try:
temp_ans_file.unlink()
except Exception:
pass


def handle_request(req: dict) -> dict:
"""Handle a single JSON-RPC 2.0 MCP request."""
method = req.get("method")
req_id = req.get("id")

if method == "initialize":
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": SERVER_INFO
}
}

elif method == "notifications/initialized":
# Notification from client, no response required
return None

elif method == "ping":
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {}
}

elif method == "tools/list":
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"tools": TOOLS
}
}

elif method == "tools/call":
params = req.get("params") or {}
tool_name = params.get("name")
args = params.get("arguments") or {}

if tool_name == "memory_recall":
query = args.get("query", "")
mode = args.get("mode", "associative")
if not query:
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": "Error: 'query' argument is required."}],
"isError": True
}
}
result_text = run_recall(query, mode=mode)
return {
"jsonrpc": "2.0",
"id": req_id,
"result": {
"content": [{"type": "text", "text": result_text}],
"isError": False
}
}
else:
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {
"code": -32601,
"message": f"Tool not found: {tool_name}"
}
}

else:
if req_id is not None:
return {
"jsonrpc": "2.0",
"id": req_id,
"error": {
"code": -32601,
"message": f"Method not found: {method}"
}
}
return None


def main():
# Direct CLI testing mode: python examples/mcp_server.py --test "query"
if len(sys.argv) > 1 and sys.argv[1] in ("--test", "-t", "--query"):
flags = {"--test", "-t", "--query", "--direct", "--ab", "--associative"}
mode = "associative"
if "--direct" in sys.argv:
mode = "direct"
elif "--ab" in sys.argv:
mode = "ab"
query_parts = [a for a in sys.argv[1:] if a not in flags]
q = " ".join(query_parts) if query_parts else "agent memory"
print(f"=== MCP Test Query: {q} (mode={mode}) ===")
print(run_recall(q, mode=mode))
return


# Standard MCP stdio loop
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
req = json.loads(line)
except Exception as e:
sys.stderr.write(f"JSON decode error: {e}\n")
continue

resp = handle_request(req)
if resp is not None:
sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
sys.stdout.flush()


if __name__ == "__main__":
main()

Loading