Danchi17 commited on
Commit
4e6a58e
Β·
verified Β·
1 Parent(s): 46d778f

Upload mnemo_mcp.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. mnemo_mcp.py +157 -0
mnemo_mcp.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ mnemo MCP server β€” expose Agora's memory layer to ANY MCP-compatible agent.
4
+
5
+ This wraps the zero-dependency `mnemo.Mnemo` store as a Model Context Protocol stdio server, so a
6
+ Claude Code / Claude Desktop / Cursor / custom agent can use mnemo as its long-term memory: it can
7
+ `remember` facts, `recall` them value-ranked (relevance Γ— accrued value, not just recency), run the
8
+ `consolidate` "dream" pass under a keep-budget, surface `contradictions`, and read value rollups.
9
+
10
+ mnemo.py stays dependency-free; only THIS file needs the MCP SDK: pip install "mcp[cli]"
11
+
12
+ Run (stdio):
13
+ MNEMO_PATH=./agent_memory.json python -m mnemo.mnemo_mcp
14
+ or register it in an MCP client (see mnemo/README.md for a .mcp.json / claude_desktop_config.json
15
+ snippet).
16
+
17
+ Config (environment):
18
+ MNEMO_PATH where to persist memory (JSON). Default: ./mnemo_memory.json
19
+ MNEMO_EMBED_URL optional OpenAI-compatible /embeddings endpoint for SEMANTIC recall
20
+ MNEMO_EMBED_MODEL embedding model id (default: text-embedding-3-small)
21
+ MNEMO_EMBED_KEY bearer key for that endpoint
22
+ With no embedder configured, mnemo uses its lexical-overlap fallback β€” it runs anywhere, today.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import os
28
+ import sys
29
+ import urllib.request
30
+ from pathlib import Path
31
+
32
+ # Import the local zero-dep store whether launched as `python -m mnemo.mnemo_mcp` or `python mnemo_mcp.py`.
33
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
34
+ from mnemo import Mnemo # noqa: E402
35
+
36
+ try:
37
+ from mcp.server.fastmcp import FastMCP
38
+ except Exception as e: # pragma: no cover
39
+ sys.stderr.write("mnemo MCP server needs the MCP SDK: pip install \"mcp[cli]\"\n")
40
+ raise
41
+
42
+
43
+ def _make_embedder():
44
+ """Optional OpenAI-compatible embedder (zero extra deps β€” urllib). Returns None if unconfigured."""
45
+ url = os.environ.get("MNEMO_EMBED_URL", "").strip()
46
+ if not url:
47
+ return None
48
+ model = os.environ.get("MNEMO_EMBED_MODEL", "text-embedding-3-small").strip()
49
+ key = os.environ.get("MNEMO_EMBED_KEY", "").strip()
50
+
51
+ def embed(text: str):
52
+ body = json.dumps({"model": model, "input": text}).encode()
53
+ headers = {"Content-Type": "application/json"}
54
+ if key:
55
+ headers["Authorization"] = f"Bearer {key}"
56
+ req = urllib.request.Request(url, data=body, headers=headers)
57
+ with urllib.request.urlopen(req, timeout=20) as r:
58
+ return json.loads(r.read())["data"][0]["embedding"]
59
+
60
+ return embed
61
+
62
+
63
+ _PATH = os.environ.get("MNEMO_PATH", "mnemo_memory.json")
64
+ _MEM = Mnemo(_PATH, embed=_make_embedder())
65
+
66
+ mcp = FastMCP("mnemo")
67
+
68
+
69
+ @mcp.tool()
70
+ def remember(text: str, tags: list[str] | None = None, value: float = 1.0,
71
+ mtype: str | None = None, key: str | None = None) -> dict:
72
+ """Store a memory (append-only; raw text is never edited afterward). `tags` group memories into
73
+ cohorts; `value` (>=1) is its importance β€” higher-value memories outrank merely-similar ones at
74
+ recall, and recall itself nudges value up. `mtype` ∈ {episodic, semantic, procedural} sets the
75
+ decay prior β€” episodic (events) fades fast, semantic (durable facts) slow, procedural (rules /
76
+ preferences) barely; pass it when you know the kind, else it's inferred. Optional `key` is a
77
+ deterministic (subject, relation) supersession key (e.g. "billing-api::auth-method"): storing a new
78
+ value with the same key retires the old one so recall never returns the stale value β€” no similarity
79
+ threshold, no extra LLM call. Use it for facts that get updated (config, prices, versions, status).
80
+ Returns the new id."""
81
+ mid = _MEM.remember(text, tags=tags or [], value=value, mtype=mtype, key=key)
82
+ rec = next((r for r in _MEM.items if r["id"] == mid), {})
83
+ return {"id": mid, "stored": text[:120], "tags": tags or [], "value": value,
84
+ "mtype": rec.get("mtype")}
85
+
86
+
87
+ @mcp.tool()
88
+ def recall(query: str, k: int = 6) -> list[dict]:
89
+ """Retrieve the top-k memories by RELEVANCE Γ— accrued VALUE (not recency). Use this to load
90
+ relevant prior knowledge before reasoning. Returns text, tags, value, and a relevance score."""
91
+ return _MEM.recall(query, k=k)
92
+
93
+
94
+ @mcp.tool()
95
+ def consolidate(keep: int | None = None) -> dict:
96
+ """Run the consolidation 'dream' pass over ALL memories: flag universal-matcher 'hub' notes, link
97
+ near-duplicates, and (if `keep` is given) supersede the lowest-value surplus. Includes the
98
+ STATE-TOGGLE guard β€” a high-similarity pair that is a polarity clash (a preference flip) is
99
+ superseded, not merged, so recall returns the new state. ADDS a derived layer only; never edits
100
+ or deletes raw memories. Returns a report (active / hubs_flagged / linked_pairs / toggled / ...)."""
101
+ return _MEM.consolidate(keep=keep)
102
+
103
+
104
+ @mcp.tool()
105
+ def consolidate_clusters(threshold: int = 15) -> dict:
106
+ """Cluster-TRIGGERED consolidation: consolidate a semantic cluster only once it has grown past
107
+ `threshold` members β€” not a global blanket. Avoids prematurely consolidating sparse topics (raw
108
+ episodes stay the best representation) and unbounded growth in dense ones. Cheap to call often
109
+ (a no-op until a cluster is ripe). Returns clusters_total / clusters_fired / linked_pairs / ..."""
110
+ return _MEM.consolidate_clusters(threshold=threshold)
111
+
112
+
113
+ @mcp.tool()
114
+ def contradictions() -> list[dict]:
115
+ """Surface mutually-incompatible memories (related in content, opposite in polarity) for review.
116
+ It FLAGS, never auto-resolves β€” silent rewrites destroy trust. Returns the conflicting pairs."""
117
+ return _MEM.contradictions()
118
+
119
+
120
+ @mcp.tool()
121
+ def value_by_cohort() -> dict:
122
+ """Per-tag value rollup (count / total value / average). Reported at the cohort level on purpose:
123
+ at n-of-1 a single memory's value is noise; the tag/time-block is where the signal is real."""
124
+ return _MEM.value_by_cohort()
125
+
126
+
127
+ @mcp.tool()
128
+ def credit(ids: list[str], outcome: str, weight: float = 1.0) -> dict:
129
+ """Close the accuracy loop: when the work some recalled memories fed gets a real verdict β€” a forecast
130
+ resolves, a claim is ruled correct/wrong, a plan succeeds/fails β€” call credit(those ids, outcome) so
131
+ each memory's track record updates. Future `recall` then ranks by WAS-IT-RIGHT (a Beta good/bad
132
+ posterior), not merely by being-recalled. `outcome`: 'good'/'right'/'correct' vs 'bad'/'wrong'/'failed'
133
+ (or pass a bool / a signed number). Counts only grow; raw text is never edited. Returns what updated."""
134
+ return _MEM.credit(ids, outcome, weight=weight)
135
+
136
+
137
+ @mcp.tool()
138
+ def forget(ids: list[str] | None = None, where_contains: str | None = None) -> dict:
139
+ """TRULY DELETE memories β€” the one op that removes content (everything else is append-only: supersession
140
+ only demotes). Use for an erasure / right-to-be-forgotten request, a poisoned or false memory, or a hard
141
+ correction. Pass `ids` (memory ids to drop) and/or `where_contains` (delete every memory whose text
142
+ contains this substring, case-insensitive). Verified forgetting: the records are deleted AND their ids are
143
+ scrubbed from every survivor's links + supersession pointers + the caches, so a forgotten memory cannot
144
+ resurface via recall or a later consolidation pass. Returns {forgotten, ids, scrubbed_links}."""
145
+ where = None
146
+ if where_contains:
147
+ needle = where_contains.lower()
148
+ where = lambda r: needle in (r.get("text") or "").lower()
149
+ return _MEM.forget(ids=ids, where=where)
150
+
151
+
152
+ def main():
153
+ mcp.run()
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()