Skip to content

Commit 17db78c

Browse files
dzmitrys-devclaude
andcommitted
release: v0.1.3 — dual_memory_write tool + qdrant_find/qdrant_store aliases
Closes the write-path gap so supamem can be the ONLY memory MCP server per project, replacing the upstream mcp-server-qdrant + dev_memory collection combo for both read and write. Added: - dual_memory_write MCP tool — writes Markdown with YAML frontmatter to <project>/.claude/insights/_agent/<slug>.md, immediately upserts into the tuned-hybrid Qdrant collection (wait=True). Idempotent on topic via UUIDv5(NAMESPACE_AGENT_WRITE, slug). Path-traversal guarded; size-capped. - qdrant_find / qdrant_store backward-compat aliases (default-on, disable with SUPAMEM_QDRANT_ALIASES=0). Lets existing prose referencing the upstream tool names keep working without rewrites. - supamem.memory_writer public module for non-MCP callers. Partial-failure: file is written before Qdrant upsert; if index fails the file persists with indexed=false in the result so next `supamem index` picks it up. PyYAML promoted to direct dep. 208/208 tests green (20 new for memory_writer); ruff clean; twine check PASSED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a03be86 commit 17db78c

7 files changed

Lines changed: 2524 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,51 @@
22

33
All notable changes to `supamem` will be documented in this file.
44

5+
## v0.1.3 — 2026-04-29
6+
7+
Adds the missing **write path** so supamem can serve as the *only* memory
8+
layer per project (no need to keep upstream `mcp-server-qdrant` alongside
9+
for the `qdrant-store` workflow).
10+
11+
### Added
12+
13+
- New `dual_memory_write` MCP tool: agents persist insights/research
14+
findings mid-session. Writes a deterministic Markdown file with YAML
15+
frontmatter to `<project>/.claude/insights/_agent/<slug>.md` and
16+
immediately upserts it into the project's tuned-hybrid Qdrant collection
17+
with `wait=True` so the very next `dual_memory_search` sees it.
18+
- Idempotent on `topic`: same topic → same `slugify()` slug → same on-disk
19+
path → same `UUIDv5(NAMESPACE_AGENT_WRITE, slug)` Qdrant point id.
20+
Re-saving overwrites in place.
21+
- Backward-compat aliases for upstream `mcp-server-qdrant` users:
22+
`qdrant_find` (alias of `dual_memory_search`) and `qdrant_store` (alias
23+
of `dual_memory_write`). Default-on; disable with
24+
`SUPAMEM_QDRANT_ALIASES=0`. Lets existing prose / agent instructions
25+
("save with qdrant-store", "query qdrant-find") keep working without
26+
rewrites.
27+
- New `supamem.memory_writer` public module: `write_memory()` for non-MCP
28+
callers (CLI plugins, scripts).
29+
30+
### Fixed
31+
32+
- Partial-failure semantics: if Qdrant indexing fails after the on-disk
33+
write succeeded, return `indexed: false` with the error message instead
34+
of leaving the file unwritten — the file is still valuable and the next
35+
`supamem index --target tuned` run will pick it up.
36+
37+
### Security
38+
39+
- Path-traversal hardened: target path resolution refuses anything that
40+
doesn't `is_relative_to(project_root)`.
41+
- Size limits enforced: `topic <= 120`, `content <= 64K`, `description
42+
<= 300`, `tags <= 10` items × 32 chars each.
43+
44+
### Added (deps)
45+
46+
- `PyYAML>=6.0` promoted from transitive to direct dependency
47+
(`memory_writer` writes YAML frontmatter; explicit dep avoids surprise
48+
removal if a transitive provider drops it).
49+
550
## v0.1.2 — 2026-04-29
651

752
Project-tunable regress baselines and config-resolved goldens path. Unblocks

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "supamem"
7-
version = "0.1.2"
7+
version = "0.1.3"
88
description = "Project-agnostic dual-memory tooling for Claude Code, Cursor, and opencode"
99
readme = "README.md"
1010
license = "MIT"
@@ -21,6 +21,7 @@ dependencies = [
2121
"langchain-text-splitters>=0.3",
2222
"platformdirs>=4.2",
2323
"packaging>=23.0",
24+
"PyYAML>=6.0",
2425
]
2526

2627
[project.urls]

src/supamem/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""supamem — project-agnostic dual-memory tooling."""
2-
__version__ = "0.1.2"
2+
__version__ = "0.1.3"

src/supamem/mcp_server.py

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,19 @@ async def dual_memory_search(
196196

197197

198198
def _register_dual_memory_tool(app: Any, config: ResolvedConfig) -> None:
199-
"""Register dual_memory_search on a FastMCP app with brand polish.
199+
"""Register dual_memory_search + dual_memory_write (and aliases) on a FastMCP app.
200200
201201
- title (spec ≥ 2025-03-26) — Cursor / Claude.ai web render this.
202-
- annotations.readOnlyHint=True — agent UIs hide the destructive-action confirmation.
203202
- description — concise; renders in tool-picker UIs.
203+
- Aliases ``qdrant_find`` / ``qdrant_store`` are registered by default for
204+
backward compat with the upstream ``mcp-server-qdrant`` tool names.
205+
Disable with ``SUPAMEM_QDRANT_ALIASES=0``.
204206
"""
205207
from mcp.server.fastmcp.tools import Tool as _FastMCPTool # noqa: F401 (typecheck only)
206208

209+
aliases_enabled = os.environ.get("SUPAMEM_QDRANT_ALIASES", "1").strip() not in ("0", "false", "")
210+
211+
# ── Read: dual_memory_search (canonical) ────────────────────────────────
207212
@app.tool(
208213
name="dual_memory_search",
209214
title=_TOOL_TITLE,
@@ -218,6 +223,115 @@ async def dual_memory_search_tool( # noqa: ARG001 (FastMCP wraps this)
218223
) -> SearchResult:
219224
return await dual_memory_search(query=query, top_k=top_k, config=config)
220225

226+
# ── Write: dual_memory_write (canonical) ────────────────────────────────
227+
@app.tool(
228+
name="dual_memory_write",
229+
title="🧠 supamem · Memory Save",
230+
description=(
231+
"Persist an insight, research finding, or note into the project's "
232+
"dual-memory corpus. Writes a Markdown file under "
233+
"<project>/.claude/insights/_agent/<slug>.md AND immediately indexes "
234+
"it into Qdrant so the very next dual_memory_search sees it. "
235+
"Idempotent on topic — re-saving the same topic overwrites in place."
236+
),
237+
)
238+
async def dual_memory_write_tool( # noqa: ARG001
239+
topic: str = Field(
240+
...,
241+
description="Short topic (used as deterministic slug; max 120 chars).",
242+
),
243+
content: str = Field(
244+
...,
245+
description="Markdown body of the memory (max 64K chars).",
246+
),
247+
description: Optional[str] = Field(
248+
None,
249+
description="Optional one-line description for the YAML frontmatter (max 300 chars).",
250+
),
251+
tags: Optional[list[str]] = Field(
252+
None,
253+
description="Optional tags (max 10, each max 32 chars).",
254+
),
255+
) -> dict:
256+
from supamem.memory_writer import write_memory
257+
258+
try:
259+
res = await asyncio.to_thread(
260+
write_memory,
261+
topic=topic,
262+
content=content,
263+
description=description,
264+
tags=tags,
265+
config=config,
266+
)
267+
except ValueError as exc:
268+
raise RuntimeError(f"dual_memory_write: {exc}") from None
269+
return {
270+
"summary": res.summary,
271+
"path": res.path,
272+
"topic": res.topic,
273+
"slug": res.slug,
274+
"indexed": res.indexed,
275+
"points_added": res.points_added,
276+
"error": res.error,
277+
}
278+
279+
# ── Backward-compat aliases for upstream `mcp-server-qdrant` users ──────
280+
if aliases_enabled:
281+
282+
@app.tool(
283+
name="qdrant_find",
284+
title="🧠 supamem · qdrant-find (alias)",
285+
description=(
286+
"Backward-compat alias for dual_memory_search. Identical behavior; "
287+
"kept so prose referencing the upstream `qdrant-find` tool name "
288+
"still routes correctly. Disable with SUPAMEM_QDRANT_ALIASES=0."
289+
),
290+
)
291+
async def qdrant_find_alias( # noqa: ARG001
292+
query: str = Field("", description="Search query (alias of dual_memory_search)."),
293+
top_k: int = Field(5, description="Max chunks to return."),
294+
) -> SearchResult:
295+
return await dual_memory_search(query=query, top_k=top_k, config=config)
296+
297+
@app.tool(
298+
name="qdrant_store",
299+
title="🧠 supamem · qdrant-store (alias)",
300+
description=(
301+
"Backward-compat alias for dual_memory_write. Identical behavior; "
302+
"kept so prose referencing the upstream `qdrant-store` tool name "
303+
"still routes correctly. Disable with SUPAMEM_QDRANT_ALIASES=0."
304+
),
305+
)
306+
async def qdrant_store_alias( # noqa: ARG001
307+
topic: str = Field(..., description="Topic (alias of dual_memory_write)."),
308+
content: str = Field(..., description="Markdown body."),
309+
description: Optional[str] = Field(None, description="Optional description."),
310+
tags: Optional[list[str]] = Field(None, description="Optional tags."),
311+
) -> dict:
312+
from supamem.memory_writer import write_memory
313+
314+
try:
315+
res = await asyncio.to_thread(
316+
write_memory,
317+
topic=topic,
318+
content=content,
319+
description=description,
320+
tags=tags,
321+
config=config,
322+
)
323+
except ValueError as exc:
324+
raise RuntimeError(f"qdrant_store: {exc}") from None
325+
return {
326+
"summary": res.summary,
327+
"path": res.path,
328+
"topic": res.topic,
329+
"slug": res.slug,
330+
"indexed": res.indexed,
331+
"points_added": res.points_added,
332+
"error": res.error,
333+
}
334+
221335

222336
def build_app(config: ResolvedConfig) -> Any:
223337
"""Construct a FastMCP app with the dual_memory_search tool registered."""

0 commit comments

Comments
 (0)