|
1 | | -"""Session memory tools: remember, recall, list_memory.""" |
| 1 | +"""Persistent memory tools: remember, recall, list_memory. |
2 | 2 |
|
3 | | -# In-process session store (reset per server process) |
4 | | -_store: dict[str, str] = {} |
| 3 | +Backed by SQLite so memory survives server restarts (the agent spawns a fresh |
| 4 | +MCP server subprocess per run). Override the location with THINKMCP_MEMORY_DB; |
| 5 | +it defaults to ./thinkmcp_memory.db in the working directory. |
| 6 | +""" |
| 7 | + |
| 8 | +import os |
| 9 | +import sqlite3 |
| 10 | +from datetime import datetime |
| 11 | + |
| 12 | + |
| 13 | +def _db_path() -> str: |
| 14 | + return os.environ.get("THINKMCP_MEMORY_DB", "./thinkmcp_memory.db") |
| 15 | + |
| 16 | + |
| 17 | +def _connect() -> sqlite3.Connection: |
| 18 | + conn = sqlite3.connect(_db_path()) |
| 19 | + conn.execute( |
| 20 | + "CREATE TABLE IF NOT EXISTS memory (" |
| 21 | + "key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL)" |
| 22 | + ) |
| 23 | + return conn |
| 24 | + |
| 25 | + |
| 26 | +def _preview(value: str, limit: int) -> str: |
| 27 | + return value[:limit] + ("..." if len(value) > limit else "") |
5 | 28 |
|
6 | 29 |
|
7 | 30 | def remember(key: str, value: str) -> dict: |
8 | | - """Persist a key-value pair for the duration of this session.""" |
9 | | - _store[key] = value |
| 31 | + """Persist a key-value pair (upsert).""" |
| 32 | + with _connect() as conn: |
| 33 | + conn.execute( |
| 34 | + "INSERT INTO memory (key, value, updated_at) VALUES (?, ?, ?) " |
| 35 | + "ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at", |
| 36 | + (key, value, datetime.now().isoformat()), |
| 37 | + ) |
| 38 | + total = conn.execute("SELECT COUNT(*) FROM memory").fetchone()[0] |
10 | 39 | return { |
11 | 40 | "status": "stored", |
12 | 41 | "key": key, |
13 | | - "value_preview": value[:100] + ("..." if len(value) > 100 else ""), |
14 | | - "total_keys": len(_store), |
| 42 | + "value_preview": _preview(value, 100), |
| 43 | + "total_keys": total, |
15 | 44 | } |
16 | 45 |
|
17 | 46 |
|
18 | 47 | def recall(key: str) -> dict: |
19 | 48 | """Retrieve a previously stored value by key.""" |
20 | | - if key not in _store: |
21 | | - available = list(_store.keys()) |
22 | | - return { |
23 | | - "status": "not_found", |
24 | | - "key": key, |
25 | | - "value": None, |
26 | | - "available_keys": available, |
27 | | - } |
28 | | - return {"status": "found", "key": key, "value": _store[key]} |
| 49 | + with _connect() as conn: |
| 50 | + row = conn.execute("SELECT value FROM memory WHERE key = ?", (key,)).fetchone() |
| 51 | + if row is None: |
| 52 | + available = [r[0] for r in conn.execute("SELECT key FROM memory ORDER BY key")] |
| 53 | + return { |
| 54 | + "status": "not_found", |
| 55 | + "key": key, |
| 56 | + "value": None, |
| 57 | + "available_keys": available, |
| 58 | + } |
| 59 | + return {"status": "found", "key": key, "value": row[0]} |
29 | 60 |
|
30 | 61 |
|
31 | 62 | def list_memory() -> dict: |
32 | | - """List all stored key-value pairs.""" |
| 63 | + """List all stored key-value pairs (values previewed).""" |
| 64 | + with _connect() as conn: |
| 65 | + rows = conn.execute("SELECT key, value FROM memory ORDER BY key").fetchall() |
33 | 66 | return { |
34 | | - "total": len(_store), |
35 | | - "entries": {k: (v[:80] + "..." if len(v) > 80 else v) for k, v in _store.items()}, |
| 67 | + "total": len(rows), |
| 68 | + "entries": {k: _preview(v, 80) for k, v in rows}, |
36 | 69 | } |
37 | 70 |
|
38 | 71 |
|
39 | 72 | def clear_memory() -> dict: |
40 | | - """Clear all stored memory (utility, not exposed as MCP tool).""" |
41 | | - count = len(_store) |
42 | | - _store.clear() |
| 73 | + """Clear all stored memory (utility, not exposed as an MCP tool).""" |
| 74 | + with _connect() as conn: |
| 75 | + count = conn.execute("SELECT COUNT(*) FROM memory").fetchone()[0] |
| 76 | + conn.execute("DELETE FROM memory") |
43 | 77 | return {"cleared": count} |
0 commit comments