brainmemory-mcp 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brainmemory_mcp/__init__.py +16 -0
- brainmemory_mcp/__main__.py +63 -0
- brainmemory_mcp/memory.py +318 -0
- brainmemory_mcp/server.py +214 -0
- brainmemory_mcp-0.1.0.dist-info/METADATA +139 -0
- brainmemory_mcp-0.1.0.dist-info/RECORD +10 -0
- brainmemory_mcp-0.1.0.dist-info/WHEEL +5 -0
- brainmemory_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- brainmemory_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- brainmemory_mcp-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""BrainMemory-MCP — Cognitive memory tools served over HTTP + SSE.
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that gives AI/LLM agents a durable
|
|
4
|
+
"brain memory": the ability to store, recall, search, update, summarize, and
|
|
5
|
+
forget information across sessions through standardized MCP tool calls.
|
|
6
|
+
|
|
7
|
+
Memory is persisted locally under ``~/.brainmemory-mcp``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
from .memory import MemoryStore, Memory, default_data_dir
|
|
15
|
+
|
|
16
|
+
__all__ = ["MemoryStore", "Memory", "default_data_dir", "__version__"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Command-line entry point for BrainMemory-MCP.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
brainmemory-mcp --host 0.0.0.0 --port 8765
|
|
5
|
+
python3 -m brainmemory_mcp
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .memory import DATA_DIR_ENV, default_data_dir
|
|
15
|
+
from .server import run
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="brainmemory-mcp",
|
|
21
|
+
description=(
|
|
22
|
+
"BrainMemory-MCP — Cognitive memory tools for AI agents, served over "
|
|
23
|
+
"the Model Context Protocol using HTTP + SSE."
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--host",
|
|
28
|
+
default=os.environ.get("BRAINMEMORY_HOST", "127.0.0.1"),
|
|
29
|
+
help="Host/interface to bind (default: 127.0.0.1).",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--port",
|
|
33
|
+
type=int,
|
|
34
|
+
default=int(os.environ.get("BRAINMEMORY_PORT", "8765")),
|
|
35
|
+
help="TCP port to listen on (default: 8765).",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--data-dir",
|
|
39
|
+
default=None,
|
|
40
|
+
help=(
|
|
41
|
+
"Directory to store memories in. "
|
|
42
|
+
f"Overrides ${DATA_DIR_ENV}. Default: {default_data_dir()}"
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--version",
|
|
47
|
+
action="version",
|
|
48
|
+
version=f"%(prog)s {__version__}",
|
|
49
|
+
)
|
|
50
|
+
return parser
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main(argv: list[str] | None = None) -> int:
|
|
54
|
+
args = build_parser().parse_args(argv)
|
|
55
|
+
try:
|
|
56
|
+
run(host=args.host, port=args.port, data_dir=args.data_dir)
|
|
57
|
+
except KeyboardInterrupt: # pragma: no cover - interactive shutdown
|
|
58
|
+
print("\nBrainMemory-MCP stopped.", flush=True)
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__": # pragma: no cover
|
|
63
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Persistence layer for BrainMemory-MCP.
|
|
2
|
+
|
|
3
|
+
Memories are stored in a local SQLite database under ``~/.brainmemory-mcp``.
|
|
4
|
+
The store is intentionally small, deterministic, and dependency-free (stdlib
|
|
5
|
+
``sqlite3`` only) so that the "brain memory" is safe and never silently loses
|
|
6
|
+
data.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sqlite3
|
|
14
|
+
import uuid
|
|
15
|
+
from dataclasses import dataclass, asdict
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Iterable
|
|
19
|
+
|
|
20
|
+
# --------------------------------------------------------------------------- #
|
|
21
|
+
# Paths
|
|
22
|
+
# --------------------------------------------------------------------------- #
|
|
23
|
+
|
|
24
|
+
DATA_DIR_ENV = "BRAINMEMORY_HOME"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def default_data_dir() -> Path:
|
|
28
|
+
"""Return the directory where memories are persisted.
|
|
29
|
+
|
|
30
|
+
Defaults to ``~/.brainmemory-mcp`` but can be overridden with the
|
|
31
|
+
``BRAINMEMORY_HOME`` environment variable.
|
|
32
|
+
"""
|
|
33
|
+
override = os.environ.get(DATA_DIR_ENV)
|
|
34
|
+
if override:
|
|
35
|
+
return Path(override).expanduser().resolve()
|
|
36
|
+
return Path.home() / ".brainmemory-mcp"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _utcnow() -> str:
|
|
40
|
+
return datetime.now(timezone.utc).isoformat()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------- #
|
|
44
|
+
# Model
|
|
45
|
+
# --------------------------------------------------------------------------- #
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Memory:
|
|
50
|
+
"""A single stored memory."""
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
content: str
|
|
54
|
+
category: str
|
|
55
|
+
tags: list[str]
|
|
56
|
+
importance: int
|
|
57
|
+
created_at: str
|
|
58
|
+
updated_at: str
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict[str, Any]:
|
|
61
|
+
return asdict(self)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _row_to_memory(row: sqlite3.Row) -> Memory:
|
|
65
|
+
return Memory(
|
|
66
|
+
id=row["id"],
|
|
67
|
+
content=row["content"],
|
|
68
|
+
category=row["category"],
|
|
69
|
+
tags=json.loads(row["tags"]) if row["tags"] else [],
|
|
70
|
+
importance=row["importance"],
|
|
71
|
+
created_at=row["created_at"],
|
|
72
|
+
updated_at=row["updated_at"],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# --------------------------------------------------------------------------- #
|
|
77
|
+
# Store
|
|
78
|
+
# --------------------------------------------------------------------------- #
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MemoryStore:
|
|
82
|
+
"""SQLite-backed memory store.
|
|
83
|
+
|
|
84
|
+
Thread/async note: each public method opens a short-lived connection, so the
|
|
85
|
+
store is safe to use from the MCP server's request handlers.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, data_dir: Path | None = None) -> None:
|
|
89
|
+
self.data_dir = Path(data_dir) if data_dir else default_data_dir()
|
|
90
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
self.db_path = self.data_dir / "memory.db"
|
|
92
|
+
self._init_db()
|
|
93
|
+
|
|
94
|
+
# -- internal ----------------------------------------------------------- #
|
|
95
|
+
|
|
96
|
+
def _connect(self) -> sqlite3.Connection:
|
|
97
|
+
conn = sqlite3.connect(self.db_path)
|
|
98
|
+
conn.row_factory = sqlite3.Row
|
|
99
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
100
|
+
conn.execute("PRAGMA foreign_keys=ON;")
|
|
101
|
+
return conn
|
|
102
|
+
|
|
103
|
+
def _init_db(self) -> None:
|
|
104
|
+
with self._connect() as conn:
|
|
105
|
+
conn.execute(
|
|
106
|
+
"""
|
|
107
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
content TEXT NOT NULL,
|
|
110
|
+
category TEXT NOT NULL DEFAULT 'general',
|
|
111
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
112
|
+
importance INTEGER NOT NULL DEFAULT 3,
|
|
113
|
+
created_at TEXT NOT NULL,
|
|
114
|
+
updated_at TEXT NOT NULL
|
|
115
|
+
);
|
|
116
|
+
"""
|
|
117
|
+
)
|
|
118
|
+
conn.execute(
|
|
119
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);"
|
|
120
|
+
)
|
|
121
|
+
conn.execute(
|
|
122
|
+
"CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);"
|
|
123
|
+
)
|
|
124
|
+
conn.commit()
|
|
125
|
+
|
|
126
|
+
# -- public API --------------------------------------------------------- #
|
|
127
|
+
|
|
128
|
+
def store(
|
|
129
|
+
self,
|
|
130
|
+
content: str,
|
|
131
|
+
*,
|
|
132
|
+
category: str = "general",
|
|
133
|
+
tags: Iterable[str] | None = None,
|
|
134
|
+
importance: int = 3,
|
|
135
|
+
) -> Memory:
|
|
136
|
+
"""Persist a new memory and return it."""
|
|
137
|
+
if not content or not content.strip():
|
|
138
|
+
raise ValueError("content must not be empty")
|
|
139
|
+
importance = max(1, min(5, int(importance)))
|
|
140
|
+
now = _utcnow()
|
|
141
|
+
mem = Memory(
|
|
142
|
+
id=uuid.uuid4().hex,
|
|
143
|
+
content=content.strip(),
|
|
144
|
+
category=(category or "general").strip() or "general",
|
|
145
|
+
tags=sorted({t.strip() for t in (tags or []) if t and t.strip()}),
|
|
146
|
+
importance=importance,
|
|
147
|
+
created_at=now,
|
|
148
|
+
updated_at=now,
|
|
149
|
+
)
|
|
150
|
+
with self._connect() as conn:
|
|
151
|
+
conn.execute(
|
|
152
|
+
"""
|
|
153
|
+
INSERT INTO memories
|
|
154
|
+
(id, content, category, tags, importance, created_at, updated_at)
|
|
155
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
156
|
+
""",
|
|
157
|
+
(
|
|
158
|
+
mem.id,
|
|
159
|
+
mem.content,
|
|
160
|
+
mem.category,
|
|
161
|
+
json.dumps(mem.tags),
|
|
162
|
+
mem.importance,
|
|
163
|
+
mem.created_at,
|
|
164
|
+
mem.updated_at,
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
conn.commit()
|
|
168
|
+
return mem
|
|
169
|
+
|
|
170
|
+
def get(self, memory_id: str) -> Memory | None:
|
|
171
|
+
"""Return a single memory by id, or ``None`` if it does not exist."""
|
|
172
|
+
with self._connect() as conn:
|
|
173
|
+
row = conn.execute(
|
|
174
|
+
"SELECT * FROM memories WHERE id = ?", (memory_id,)
|
|
175
|
+
).fetchone()
|
|
176
|
+
return _row_to_memory(row) if row else None
|
|
177
|
+
|
|
178
|
+
def search(
|
|
179
|
+
self,
|
|
180
|
+
query: str | None = None,
|
|
181
|
+
*,
|
|
182
|
+
category: str | None = None,
|
|
183
|
+
tags: Iterable[str] | None = None,
|
|
184
|
+
min_importance: int | None = None,
|
|
185
|
+
limit: int = 20,
|
|
186
|
+
) -> list[Memory]:
|
|
187
|
+
"""Search memories by free text, category, tags and/or importance."""
|
|
188
|
+
clauses: list[str] = []
|
|
189
|
+
params: list[Any] = []
|
|
190
|
+
|
|
191
|
+
if query and query.strip():
|
|
192
|
+
clauses.append("(content LIKE ? OR tags LIKE ? OR category LIKE ?)")
|
|
193
|
+
like = f"%{query.strip()}%"
|
|
194
|
+
params.extend([like, like, like])
|
|
195
|
+
|
|
196
|
+
if category and category.strip():
|
|
197
|
+
clauses.append("category = ?")
|
|
198
|
+
params.append(category.strip())
|
|
199
|
+
|
|
200
|
+
if min_importance is not None:
|
|
201
|
+
clauses.append("importance >= ?")
|
|
202
|
+
params.append(max(1, min(5, int(min_importance))))
|
|
203
|
+
|
|
204
|
+
wanted_tags = [t.strip() for t in (tags or []) if t and t.strip()]
|
|
205
|
+
for tag in wanted_tags:
|
|
206
|
+
clauses.append("tags LIKE ?")
|
|
207
|
+
params.append(f'%"{tag}"%')
|
|
208
|
+
|
|
209
|
+
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
210
|
+
sql = (
|
|
211
|
+
f"SELECT * FROM memories {where} "
|
|
212
|
+
"ORDER BY importance DESC, updated_at DESC LIMIT ?"
|
|
213
|
+
)
|
|
214
|
+
params.append(max(1, int(limit)))
|
|
215
|
+
|
|
216
|
+
with self._connect() as conn:
|
|
217
|
+
rows = conn.execute(sql, params).fetchall()
|
|
218
|
+
return [_row_to_memory(r) for r in rows]
|
|
219
|
+
|
|
220
|
+
def list_all(self, *, limit: int = 100, offset: int = 0) -> list[Memory]:
|
|
221
|
+
"""Return memories ordered by importance then recency."""
|
|
222
|
+
with self._connect() as conn:
|
|
223
|
+
rows = conn.execute(
|
|
224
|
+
"SELECT * FROM memories "
|
|
225
|
+
"ORDER BY importance DESC, updated_at DESC "
|
|
226
|
+
"LIMIT ? OFFSET ?",
|
|
227
|
+
(max(1, int(limit)), max(0, int(offset))),
|
|
228
|
+
).fetchall()
|
|
229
|
+
return [_row_to_memory(r) for r in rows]
|
|
230
|
+
|
|
231
|
+
def update(
|
|
232
|
+
self,
|
|
233
|
+
memory_id: str,
|
|
234
|
+
*,
|
|
235
|
+
content: str | None = None,
|
|
236
|
+
category: str | None = None,
|
|
237
|
+
tags: Iterable[str] | None = None,
|
|
238
|
+
importance: int | None = None,
|
|
239
|
+
) -> Memory | None:
|
|
240
|
+
"""Update fields of an existing memory. Returns the updated memory."""
|
|
241
|
+
current = self.get(memory_id)
|
|
242
|
+
if current is None:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if content is not None and content.strip():
|
|
246
|
+
current.content = content.strip()
|
|
247
|
+
if category is not None and category.strip():
|
|
248
|
+
current.category = category.strip()
|
|
249
|
+
if tags is not None:
|
|
250
|
+
current.tags = sorted({t.strip() for t in tags if t and t.strip()})
|
|
251
|
+
if importance is not None:
|
|
252
|
+
current.importance = max(1, min(5, int(importance)))
|
|
253
|
+
current.updated_at = _utcnow()
|
|
254
|
+
|
|
255
|
+
with self._connect() as conn:
|
|
256
|
+
conn.execute(
|
|
257
|
+
"""
|
|
258
|
+
UPDATE memories
|
|
259
|
+
SET content = ?, category = ?, tags = ?, importance = ?, updated_at = ?
|
|
260
|
+
WHERE id = ?
|
|
261
|
+
""",
|
|
262
|
+
(
|
|
263
|
+
current.content,
|
|
264
|
+
current.category,
|
|
265
|
+
json.dumps(current.tags),
|
|
266
|
+
current.importance,
|
|
267
|
+
current.updated_at,
|
|
268
|
+
current.id,
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
conn.commit()
|
|
272
|
+
return current
|
|
273
|
+
|
|
274
|
+
def forget(self, memory_id: str) -> bool:
|
|
275
|
+
"""Delete a memory by id. Returns True if a row was removed."""
|
|
276
|
+
with self._connect() as conn:
|
|
277
|
+
cur = conn.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
|
|
278
|
+
conn.commit()
|
|
279
|
+
return cur.rowcount > 0
|
|
280
|
+
|
|
281
|
+
def clear(self) -> int:
|
|
282
|
+
"""Delete ALL memories. Returns the number of rows removed."""
|
|
283
|
+
with self._connect() as conn:
|
|
284
|
+
cur = conn.execute("DELETE FROM memories")
|
|
285
|
+
conn.commit()
|
|
286
|
+
return cur.rowcount
|
|
287
|
+
|
|
288
|
+
def stats(self) -> dict[str, Any]:
|
|
289
|
+
"""Return summary statistics about the stored memories."""
|
|
290
|
+
with self._connect() as conn:
|
|
291
|
+
total = conn.execute("SELECT COUNT(*) AS c FROM memories").fetchone()["c"]
|
|
292
|
+
by_category = {
|
|
293
|
+
row["category"]: row["c"]
|
|
294
|
+
for row in conn.execute(
|
|
295
|
+
"SELECT category, COUNT(*) AS c FROM memories "
|
|
296
|
+
"GROUP BY category ORDER BY c DESC"
|
|
297
|
+
).fetchall()
|
|
298
|
+
}
|
|
299
|
+
avg_importance_row = conn.execute(
|
|
300
|
+
"SELECT AVG(importance) AS a FROM memories"
|
|
301
|
+
).fetchone()
|
|
302
|
+
avg_importance = round(avg_importance_row["a"], 2) if avg_importance_row["a"] else 0
|
|
303
|
+
|
|
304
|
+
all_tags: dict[str, int] = {}
|
|
305
|
+
for row in conn.execute("SELECT tags FROM memories").fetchall():
|
|
306
|
+
for tag in json.loads(row["tags"]) if row["tags"] else []:
|
|
307
|
+
all_tags[tag] = all_tags.get(tag, 0) + 1
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"total": total,
|
|
311
|
+
"by_category": by_category,
|
|
312
|
+
"top_tags": dict(
|
|
313
|
+
sorted(all_tags.items(), key=lambda kv: kv[1], reverse=True)[:10]
|
|
314
|
+
),
|
|
315
|
+
"average_importance": avg_importance,
|
|
316
|
+
"data_dir": str(self.data_dir),
|
|
317
|
+
"db_path": str(self.db_path),
|
|
318
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""BrainMemory-MCP server.
|
|
2
|
+
|
|
3
|
+
Exposes Cognitive memory tools over the Model Context Protocol using the
|
|
4
|
+
HTTP + Server-Sent Events (SSE) transport. Built on the official ``mcp`` SDK
|
|
5
|
+
(``FastMCP``).
|
|
6
|
+
|
|
7
|
+
Cognitive tools:
|
|
8
|
+
- store_memory : persist a new memory
|
|
9
|
+
- recall_memory : fetch a single memory by id
|
|
10
|
+
- search_memory : find memories by text / category / tags / importance
|
|
11
|
+
- list_memories : list stored memories (most important first)
|
|
12
|
+
- update_memory : modify an existing memory
|
|
13
|
+
- forget_memory : delete a memory
|
|
14
|
+
- summarize_memories : summary statistics over stored memories
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from mcp.server.fastmcp import FastMCP
|
|
22
|
+
|
|
23
|
+
from .memory import MemoryStore, default_data_dir
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------------------------------- #
|
|
26
|
+
# Factory
|
|
27
|
+
# --------------------------------------------------------------------------- #
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_server(
|
|
31
|
+
*,
|
|
32
|
+
host: str = "127.0.0.1",
|
|
33
|
+
port: int = 8765,
|
|
34
|
+
data_dir: str | None = None,
|
|
35
|
+
) -> FastMCP:
|
|
36
|
+
"""Create and configure the BrainMemory-MCP :class:`FastMCP` server."""
|
|
37
|
+
|
|
38
|
+
store = MemoryStore(data_dir=data_dir) if data_dir else MemoryStore()
|
|
39
|
+
|
|
40
|
+
mcp = FastMCP(
|
|
41
|
+
name="BrainMemory-MCP",
|
|
42
|
+
instructions=(
|
|
43
|
+
"Cognitive brain-memory for AI agents. Use these tools to persist "
|
|
44
|
+
"durable memories across sessions, then recall/search them later. "
|
|
45
|
+
"Store concise, self-contained facts; tag them and set an "
|
|
46
|
+
"importance from 1 (trivial) to 5 (critical). Search before "
|
|
47
|
+
"storing to avoid duplicates."
|
|
48
|
+
),
|
|
49
|
+
host=host,
|
|
50
|
+
port=port,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# ----------------------------------------------------------------- tools #
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def store_memory(
|
|
57
|
+
content: str,
|
|
58
|
+
category: str = "general",
|
|
59
|
+
tags: list[str] | None = None,
|
|
60
|
+
importance: int = 3,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Persist a new memory in the brain.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
content: The information to remember (a concise, self-contained fact).
|
|
66
|
+
category: A grouping label, e.g. "preferences", "facts", "tasks".
|
|
67
|
+
tags: Optional keywords to make the memory easier to find later.
|
|
68
|
+
importance: 1 (trivial) .. 5 (critical). Defaults to 3.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The stored memory including its generated ``id``.
|
|
72
|
+
"""
|
|
73
|
+
mem = store.store(
|
|
74
|
+
content,
|
|
75
|
+
category=category,
|
|
76
|
+
tags=tags or [],
|
|
77
|
+
importance=importance,
|
|
78
|
+
)
|
|
79
|
+
return {"status": "stored", "memory": mem.to_dict()}
|
|
80
|
+
|
|
81
|
+
@mcp.tool()
|
|
82
|
+
def recall_memory(memory_id: str) -> dict[str, Any]:
|
|
83
|
+
"""Recall a single memory by its id.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
memory_id: The id returned when the memory was stored.
|
|
87
|
+
"""
|
|
88
|
+
mem = store.get(memory_id)
|
|
89
|
+
if mem is None:
|
|
90
|
+
return {"status": "not_found", "memory_id": memory_id}
|
|
91
|
+
return {"status": "ok", "memory": mem.to_dict()}
|
|
92
|
+
|
|
93
|
+
@mcp.tool()
|
|
94
|
+
def search_memory(
|
|
95
|
+
query: str = "",
|
|
96
|
+
category: str | None = None,
|
|
97
|
+
tags: list[str] | None = None,
|
|
98
|
+
min_importance: int | None = None,
|
|
99
|
+
limit: int = 20,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Search memories by free text, category, tags and/or importance.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
query: Free-text matched against content, tags and category.
|
|
105
|
+
category: Restrict to a single category.
|
|
106
|
+
tags: Only memories containing ALL of these tags.
|
|
107
|
+
min_importance: Only memories with importance >= this value (1..5).
|
|
108
|
+
limit: Maximum number of results (default 20).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A list of matching memories, most important & most recent first.
|
|
112
|
+
"""
|
|
113
|
+
results = store.search(
|
|
114
|
+
query=query or None,
|
|
115
|
+
category=category,
|
|
116
|
+
tags=tags or None,
|
|
117
|
+
min_importance=min_importance,
|
|
118
|
+
limit=limit,
|
|
119
|
+
)
|
|
120
|
+
return {
|
|
121
|
+
"status": "ok",
|
|
122
|
+
"count": len(results),
|
|
123
|
+
"memories": [m.to_dict() for m in results],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def list_memories(limit: int = 100, offset: int = 0) -> dict[str, Any]:
|
|
128
|
+
"""List stored memories, most important and most recent first.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
limit: Maximum number of memories to return (default 100).
|
|
132
|
+
offset: Number of memories to skip (for pagination).
|
|
133
|
+
"""
|
|
134
|
+
results = store.list_all(limit=limit, offset=offset)
|
|
135
|
+
return {
|
|
136
|
+
"status": "ok",
|
|
137
|
+
"count": len(results),
|
|
138
|
+
"memories": [m.to_dict() for m in results],
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@mcp.tool()
|
|
142
|
+
def update_memory(
|
|
143
|
+
memory_id: str,
|
|
144
|
+
content: str | None = None,
|
|
145
|
+
category: str | None = None,
|
|
146
|
+
tags: list[str] | None = None,
|
|
147
|
+
importance: int | None = None,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
"""Update fields of an existing memory (only provided fields change).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
memory_id: The id of the memory to update.
|
|
153
|
+
content: New content (optional).
|
|
154
|
+
category: New category (optional).
|
|
155
|
+
tags: Replacement tag list (optional).
|
|
156
|
+
importance: New importance 1..5 (optional).
|
|
157
|
+
"""
|
|
158
|
+
mem = store.update(
|
|
159
|
+
memory_id,
|
|
160
|
+
content=content,
|
|
161
|
+
category=category,
|
|
162
|
+
tags=tags,
|
|
163
|
+
importance=importance,
|
|
164
|
+
)
|
|
165
|
+
if mem is None:
|
|
166
|
+
return {"status": "not_found", "memory_id": memory_id}
|
|
167
|
+
return {"status": "updated", "memory": mem.to_dict()}
|
|
168
|
+
|
|
169
|
+
@mcp.tool()
|
|
170
|
+
def forget_memory(memory_id: str) -> dict[str, Any]:
|
|
171
|
+
"""Forget (delete) a memory by its id.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
memory_id: The id of the memory to delete.
|
|
175
|
+
"""
|
|
176
|
+
removed = store.forget(memory_id)
|
|
177
|
+
return {
|
|
178
|
+
"status": "forgotten" if removed else "not_found",
|
|
179
|
+
"memory_id": memory_id,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@mcp.tool()
|
|
183
|
+
def summarize_memories() -> dict[str, Any]:
|
|
184
|
+
"""Summarize the brain memory: totals, categories, top tags, etc."""
|
|
185
|
+
return {"status": "ok", "summary": store.stats()}
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------- resources #
|
|
188
|
+
|
|
189
|
+
@mcp.resource("brainmemory://stats")
|
|
190
|
+
def stats_resource() -> str:
|
|
191
|
+
"""Live summary statistics of the stored memories (JSON)."""
|
|
192
|
+
import json
|
|
193
|
+
|
|
194
|
+
return json.dumps(store.stats(), indent=2)
|
|
195
|
+
|
|
196
|
+
return mcp
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run(
|
|
200
|
+
*,
|
|
201
|
+
host: str = "127.0.0.1",
|
|
202
|
+
port: int = 8765,
|
|
203
|
+
data_dir: str | None = None,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Create the server and run it over the SSE transport."""
|
|
206
|
+
server = create_server(host=host, port=port, data_dir=data_dir)
|
|
207
|
+
resolved_dir = data_dir or str(default_data_dir())
|
|
208
|
+
print(
|
|
209
|
+
f"BrainMemory-MCP (SSE) listening on http://{host}:{port}/sse\n"
|
|
210
|
+
f" message endpoint : http://{host}:{port}/messages/\n"
|
|
211
|
+
f" memory directory : {resolved_dir}",
|
|
212
|
+
flush=True,
|
|
213
|
+
)
|
|
214
|
+
server.run(transport="sse")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brainmemory-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: BrainMemory-MCP — a Model Context Protocol server exposing Cognitive memory tools over HTTP + SSE.
|
|
5
|
+
Author: BrainMemory-MCP maintainers
|
|
6
|
+
Maintainer: BrainMemory-MCP maintainers
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/venturo/BrainMemory-MCP
|
|
9
|
+
Project-URL: Repository, https://github.com/venturo/BrainMemory-MCP
|
|
10
|
+
Project-URL: Issues, https://github.com/venturo/BrainMemory-MCP/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/venturo/BrainMemory-MCP/tree/main/docs/changelog
|
|
12
|
+
Keywords: mcp,model-context-protocol,memory,sse,cognitive,llm,agent
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: mcp>=1.2.0
|
|
31
|
+
Requires-Dist: starlette>=0.37.0
|
|
32
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
35
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
36
|
+
Provides-Extra: build
|
|
37
|
+
Requires-Dist: build>=1.2.0; extra == "build"
|
|
38
|
+
Requires-Dist: twine>=5.0.0; extra == "build"
|
|
39
|
+
Dynamic: license-file
|
|
40
|
+
|
|
41
|
+
# BrainMemory-MCP
|
|
42
|
+
|
|
43
|
+
A **Model Context Protocol (MCP)** server that gives AI/LLM agents a durable
|
|
44
|
+
**brain memory** — the ability to store, recall, search, summarize, and forget
|
|
45
|
+
information across sessions through standardized MCP tool calls.
|
|
46
|
+
|
|
47
|
+
The server speaks MCP over **HTTP + Server-Sent Events (SSE)**, so remote
|
|
48
|
+
MCP-capable clients (Claude, IDE agents, etc.) can connect over the network.
|
|
49
|
+
Memory is persisted locally under **`~/.brainmemory-mcp`** (a SQLite database).
|
|
50
|
+
|
|
51
|
+
## Cognitive Tools
|
|
52
|
+
|
|
53
|
+
| Tool | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `store_memory` | Persist a new memory (content, category, tags, importance). |
|
|
56
|
+
| `recall_memory` | Fetch a single memory by its id. |
|
|
57
|
+
| `search_memory` | Find memories by free text, category, tags and/or importance. |
|
|
58
|
+
| `list_memories` | List stored memories (most important & recent first). |
|
|
59
|
+
| `update_memory` | Modify an existing memory (only supplied fields change). |
|
|
60
|
+
| `forget_memory` | Delete a memory by id. |
|
|
61
|
+
| `summarize_memories` | Summary statistics: totals, categories, top tags. |
|
|
62
|
+
|
|
63
|
+
A read-only resource `brainmemory://stats` exposes the same summary as JSON.
|
|
64
|
+
|
|
65
|
+
## Install
|
|
66
|
+
|
|
67
|
+
From PyPI (once published):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 -m pip install brainmemory-mcp
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
From a local checkout:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 -m pip install .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both install the package and a console script named `brainmemory-mcp`.
|
|
80
|
+
|
|
81
|
+
For development (editable install):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python3 -m pip install -e ".[dev]"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
To build/publish a release, see [`docs/RELEASING.md`](docs/RELEASING.md).
|
|
88
|
+
|
|
89
|
+
## Run
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Defaults: 127.0.0.1:8765, memory in ~/.brainmemory-mcp
|
|
93
|
+
brainmemory-mcp
|
|
94
|
+
|
|
95
|
+
# Custom host/port and memory location
|
|
96
|
+
brainmemory-mcp --host 0.0.0.0 --port 9000 --data-dir /path/to/memory
|
|
97
|
+
|
|
98
|
+
# Or without the console script
|
|
99
|
+
python3 -m brainmemory_mcp
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Endpoints once running:
|
|
103
|
+
|
|
104
|
+
- SSE stream: `http://<host>:<port>/sse`
|
|
105
|
+
- Message POST: `http://<host>:<port>/messages/`
|
|
106
|
+
|
|
107
|
+
### Configuration
|
|
108
|
+
|
|
109
|
+
| Option | Env var | Default |
|
|
110
|
+
|--------|---------|---------|
|
|
111
|
+
| `--host` | `BRAINMEMORY_HOST` | `127.0.0.1` |
|
|
112
|
+
| `--port` | `BRAINMEMORY_PORT` | `8765` |
|
|
113
|
+
| `--data-dir` | `BRAINMEMORY_HOME` | `~/.brainmemory-mcp` |
|
|
114
|
+
|
|
115
|
+
## Connect a client
|
|
116
|
+
|
|
117
|
+
Point any MCP client that supports SSE at the `/sse` endpoint. Example client
|
|
118
|
+
configuration (URL-based transport):
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"brainmemory": {
|
|
124
|
+
"url": "http://127.0.0.1:8765/sse"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## How memory is stored
|
|
131
|
+
|
|
132
|
+
Memories live in `~/.brainmemory-mcp/memory.db` (SQLite, WAL mode). Each memory
|
|
133
|
+
has: `id`, `content`, `category`, `tags`, `importance` (1–5), `created_at`,
|
|
134
|
+
`updated_at`. Nothing is ever silently deleted — removal only happens through
|
|
135
|
+
`forget_memory`.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
brainmemory_mcp/__init__.py,sha256=W6FjnEWF8YUZmcDX4adgavxKRtA_KrePLGpAKckKlyw,546
|
|
2
|
+
brainmemory_mcp/__main__.py,sha256=BxP7hN3FL9KK-zE6F-O8qek-63wiIaySCzZ-G7aCFBQ,1717
|
|
3
|
+
brainmemory_mcp/memory.py,sha256=ZA2faPzt8Wa4PkSEU99dwV6y-uAbtDRbOJhOPoJwzQI,10902
|
|
4
|
+
brainmemory_mcp/server.py,sha256=JVqrB4Y74esCTtYqQz3nUlYSeFPEyHVrch-E6BTro_4,7068
|
|
5
|
+
brainmemory_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=g3rT7cWUZFU4U9BVCZ4XgmuNK60opz3iv6AhQ_YFPX0,1065
|
|
6
|
+
brainmemory_mcp-0.1.0.dist-info/METADATA,sha256=sJKn6iH4WqzcJxbJGC2Ayf4e4HuWKPtF6ARaTWDQIdo,4409
|
|
7
|
+
brainmemory_mcp-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
brainmemory_mcp-0.1.0.dist-info/entry_points.txt,sha256=JlyGG7NZAbv-H-UMSP6WQy669XqRVRpwpiSXkF410C0,66
|
|
9
|
+
brainmemory_mcp-0.1.0.dist-info/top_level.txt,sha256=DV-hcgx-1yiSbycccxIk4pbLYEVqsxxfZ_utSVsWg_c,16
|
|
10
|
+
brainmemory_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 R&D ICWR
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brainmemory_mcp
|