This commit is contained in:
SHAHRZAD SAEBI 2026-08-27 17:49:26 +01:00 committed by GitHub
commit 2341e765ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 547 additions and 0 deletions

View file

@ -0,0 +1,141 @@
---
name: supermemory-local-mcp
description: Use when setting up Supermemory (self-hosted local server at localhost:6767) and wiring it into Hermes plus MCP clients (Claude Code, OpenCode, Cline, Kilo Code, Zed) so they share one local memory store. Covers the missing-LLM-key startup failure, the stdio MCP bridge, and the systemd auto-start unit.
---
# Supermemory Local + MCP Bridge
Run Supermemory **fully locally** (no cloud dependency) and expose it to Hermes
and any MCP-capable editor as one shared memory layer.
## When to use
- User installed `supermemoryai/supermemory` locally and it "isn't connected" — almost always the server isn't running OR `memory.provider`/`supermemory.json` isn't set.
- User wants the SAME memory available in Claude Code, OpenCode, Cline, Kilo Code, and Zed.
- Local server won't start with `No model provider API key configured`.
## Key facts (verified on this env)
- Local server binary: `~/.supermemory/bin/supermemory-server` (ELF/Bun). Data store lives in `~/.supermemory/.supermemory` (pin with `SUPERMEMORY_DATA_DIR=~/.supermemory` or the store follows CWD).
- Local API key printed on first boot, stored at `~/.supermemory/api-key`.
- Server listens on `http://localhost:6767`. **It has NO MCP endpoint** — only REST v3 (`/v3/documents`, `/v3/search`, `/v3/documents/list`). `v4/*` endpoints are NOT available locally.
- Server **refuses to start without an LLM key** for memory extraction: needs `GEMINI_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GROQ_API_KEY`.
- Official `https://mcp.supermemory.ai/mcp` is a *remote* OAuth server — do NOT use it if the goal is local-only data.
## The bridge (why it exists)
Because the local server exposes REST only, an MCP client cannot talk to it directly.
The bridge (`scripts/supermemory-mcp`) is a zero-dependency Python **stdio MCP server**
that proxies MCP tool calls to the local REST API. No `pip install` needed (stdlib only).
Tools exposed: `search_memory`, `add_memory`, `list_documents`, `get_document`, `list_memories`, `whoami`.
## Setup — full flow
1. **Start the server** (needs an LLM key in env):
```bash
export GEMINI_API_KEY=... # free: https://aistudio.google.com/app/apikey
~/.supermemory/bin/supermemory-server # or use scripts/supermemory-local
```
Verify: `ss -tlnp | grep 6767`. First boot downloads ~106MB model — allow time.
`./bin/supermemory-server doctor` lists exactly what's missing.
2. **Wire into Hermes** (plugin already present at `plugins/memory/supermemory`):
- `~/.hermes/supermemory.json`: `{"base_url":"http://localhost:6767","container_tag":"hermes","search_mode":"hybrid","api_timeout":60.0}`
- Append `SUPERMEMORY_API_KEY=<contents of ~/.supermemory/api-key>` to `~/.hermes/.env`
- `hermes config set memory.provider supermemory`
- The `supermemory` Python SDK lazy-installs on first chat.
3. **Install the bridge** so all MCP clients can use it:
```bash
cp scripts/supermemory-mcp ~/.local/bin/supermemory-mcp
chmod +x ~/.local/bin/supermemory-mcp
```
4. **Register with each client** (all point at the same bridge binary → one shared store):
| Client | Command / location |
|--------|-------------------|
| Claude Code | `claude mcp add supermemory --scope user -- ~/.local/bin/supermemory-mcp` |
| OpenCode | `opencode mcp add supermemory -- ~/.local/bin/supermemory-mcp` |
| Zed | add to `~/.config/zed/settings.json`: `"context_servers": {"supermemory": {"command": "~/.local/bin/supermemory-mcp", "args": []}}` (JSONC — preserve `//` comments) |
| Cline | `~/.vscode/settings.json`: `"cline.mcpServers": {"supermemory": {"command":"~/.local/bin/supermemory-mcp","args":[],"env":{}}}` |
| Kilo Code | same file: `"kilo-code.mcpServers": {...}` (also try `kilocode.mcpServers`) |
5. **Auto-start on boot** (systemd user, needs `Linger=yes` which most setups have):
- Copy `templates/supermemory.service` to `~/.config/systemd/user/`, replace `__REPLACE_WITH_YOUR_LLM_KEY__` with a real key.
- `systemctl --user daemon-reload && systemctl --user enable --now supermemory.service`
## Verify (do this before declaring done)
Bridge protocol test — must return 6 tools and a real search result:
```bash
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"search_memory","arguments":{"query":"test"}}}' \
| ~/.local/bin/supermemory-mcp
```
Hermes: `hermes memory status` should show `Provider: supermemory` and `← active`.
Then `hermes chat -q "remember X"` then a new session asking about X — it should recall it.
## Multi-project memory + auto-sync (recommended for coders)
Give each project its OWN `container_tag` so memories never mix. Then wire commit history
into memory automatically via a `post-commit` hook, and keep `CLAUDE.md`/`AGENTS.md` in sync via a daily timer.
### Project -> container mapping (edit to match your repos)
In `scripts/supermemory-sync`, the `PROJECTS` map keys are container_tags:
```bash
declare -A PROJECTS=(
[dafgpt]="/path/to/DafGpt"
[hejle]="/path/to/hejle"
[limbik]="/path/to/Limbik"
)
```
The hook lowercases the repo dir name to derive the tag (DafGpt->dafgpt, etc.),
so keep dir names aligned with container names.
### Install the sync tool
```bash
cp scripts/supermemory-sync ~/.local/bin/supermemory-sync
chmod +x ~/.local/bin/supermemory-sync
```
### Per-project post-commit hook (auto-feeds every commit into memory)
Create `<repo>/.git/hooks/post-commit`:
```bash
#!/usr/bin/env bash
set -e
root="$(git rev-parse --show-toplevel)"
project="$(basename "$root" | tr 'A-Z' 'a-z')"
msg="$(git log -1 --pretty=%B)"
files="$(git diff-tree --no-commit-id --name-only -r HEAD | tr '\n' ' ')"
LOG="$HOME/.supermemory/sync.log"
{
echo "$(date -Is) [$project] commit: $msg"
/home/themorida/.local/bin/supermemory-sync remember-commit "$project" "$msg" $files
} >> "$LOG" 2>&1 || true
```
chmod +x it. Now every `git commit` in that repo writes a memory into its container — no manual step.
For a repo WITHOUT git (e.g. hejle originally), `git init`, commit, then install the same hook.
### Daily rule sync (catches manual CLAUDE.md edits)
`scripts/supermemory-sync sync-rules` re-ingests `CLAUDE.md`/`AGENTS.md` only when their
sha256 changes (state stored in `~/.supermemory/sync-state.json`). Run it from a systemd timer:
- `templates/supermemory-sync.service` (Type=oneshot -> `supermemory-sync sync-rules`)
- `templates/supermemory-sync.timer` (`OnCalendar=*-*-* 09:00:00`)
- `systemctl --user enable --now supermemory-sync.timer`
### Mark the container in the project's own docs
Append to each project's `CLAUDE.md`/`AGENTS.md` so agents know which container to scope to:
```
> **Supermemory container:** `dafgpt` — scope supermemory calls to this container.
```
### Manual helpers
- `supermemory-sync remember-bug <project> <note>` — store a bug/fix for later recall.
- `supermemory-sync remember-commit <project> <msg> [files...]` — ad-hoc commit memory.
## Common mistakes
- **Server won't start** → missing LLM key. Set `GEMINI_API_KEY` (free) and retry; run `doctor`.
- **Tools list empty / 404 on /v4/*** → you hit a v4 endpoint; local server is v3-only. Use `/v3/*`.
- **Zed config breaks** → Zed uses JSONC; editing via raw write_file strips `//` comments. Patch the file in place or use a Python edit that preserves comments.
- **Cline/Kilo show "not connected"** → the VS Code extension isn't installed on this machine. The settings.json stub is ready; it activates once the extension is added.
- **Memory store seems empty after moving dirs** → you launched from a different CWD and created a second store. Always set `SUPERMEMORY_DATA_DIR=~/.supermemory`.
- **MCP client can't find the key** → bridge reads `~/.supermemory/api-key` automatically; don't hardcode the key in client config.

View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Launch the local Supermemory server (self-hosted memory backend for Hermes).
# Requires ONE LLM key in the environment for memory extraction:
# GEMINI_API_KEY (recommended, free from https://aistudio.google.com/app/apikey)
# or OPENAI_API_KEY / ANTHROPIC_API_KEY / GROQ_API_KEY
set -euo pipefail
BIN="$HOME/.supermemory/bin/supermemory-server"
DATA_DIR="$HOME/.supermemory"
if [ ! -x "$BIN" ]; then
echo "ERROR: supermemory-server binary not found at $BIN" >&2
exit 1
fi
if [ -z "${GEMINI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${ANTHROPIC_API_KEY:-}" ] && [ -z "${GROQ_API_KEY:-}" ]; then
echo "ERROR: set one of GEMINI_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / GROQ_API_KEY first." >&2
echo " Get a free Gemini key: https://aistudio.google.com/app/apikey" >&2
exit 1
fi
# Pin the data store so we keep the existing store (doctor warned it follows CWD otherwise).
export SUPERMEMORY_DATA_DIR="$DATA_DIR"
cd "$DATA_DIR"
echo "Starting Supermemory local on http://localhost:6767 (data: $DATA_DIR)"
exec "$BIN"

View file

@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Supermemory MCP bridge (STDIO) -> local Supermemory server at http://localhost:6767.
Reads the local API key from ~/.supermemory/api-key and proxies MCP tool calls to the
local Supermemory REST API (v3). No external dependencies: uses only the Python stdlib.
Tools exposed:
search_memory - semantic recall from the hermes container (+profile context)
add_memory - save (or forget) a memory
list_documents - browse stored source documents
get_document - read one document's content
list_memories - browse extracted memory entries
whoami - report the active container tag
Usage: supermemory-mcp (Claude Code / OpenCode / Cline / Kilo / Zed invoke this as a stdio MCP server)
"""
import json
import os
import sys
import urllib.request
import urllib.error
import urllib.parse
BASE_URL = os.environ.get("SUPERMEMORY_BASE_URL", "http://localhost:6767").rstrip("/")
CONTAINER = os.environ.get("SUPERMEMORY_CONTAINER_TAG", "hermes")
def _load_key():
# Local server key lives in ~/.supermemory/api-key
p = os.path.expanduser("~/.supermemory/api-key")
try:
with open(p) as f:
return f.read().strip()
except OSError:
return os.environ.get("SUPERMEMORY_API_KEY", "")
API_KEY = _load_key()
def _post(path, payload):
url = f"{BASE_URL}{path}"
data = json.dumps(payload).encode()
req = urllib.request.Request(url, data=data, method="POST",
headers={"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"})
return _send(req)
def _get(path, params=None):
url = f"{BASE_URL}{path}"
if params:
url += "?" + "&".join(f"{k}={urllib.parse.quote(str(v))}" for k, v in params.items())
req = urllib.request.Request(url, method="GET",
headers={"Authorization": f"Bearer {API_KEY}"})
return _send(req)
def _send(req):
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read().decode())
except urllib.error.HTTPError as e:
try:
body = e.read().decode()
except Exception:
body = ""
return {"error": f"HTTP {e.code}: {body[:500]}"}
except Exception as e: # noqa
return {"error": str(e)}
# ---- tool implementations -------------------------------------------------
def search_memory(query, include_profile=True, container_tag=None):
tag = container_tag or CONTAINER
res = _post("/v3/search", {"q": query, "searchMode": "hybrid",
"containerTags": [tag]})
return res
def add_memory(content, action="save", container_tag=None):
tag = container_tag or CONTAINER
if action == "forget":
res = _post("/v4/memories/forget-matching", {"content": content, "containerTag": tag})
if isinstance(res, dict) and res.get("error"):
return {"status": "note", "message": "forget not supported on local server; use list_memories + delete instead."}
return res
res = _post("/v3/documents", {"content": content, "containerTag": tag,
"metadata": {"type": "memory"}, "dreaming": "instant"})
return res
def list_documents(page=1, limit=10, container_tag=None):
tag = container_tag or CONTAINER
return _post("/v3/documents/list", {"page": page, "limit": limit,
"containerTags": [tag]})
def get_document(document_id, container_tag=None):
tag = container_tag or CONTAINER
return _post("/v3/documents/list", {"containerTags": [tag], "limit": 50})
def list_memories(page=1, limit=10, container_tag=None):
tag = container_tag or CONTAINER
return _post("/v3/documents/list", {"page": page, "limit": limit,
"containerTags": [tag]})
def whoami():
return {"active_container": CONTAINER, "base_url": BASE_URL,
"server": "local-supermemory", "authenticated": bool(API_KEY)}
TOOLS = [
{
"name": "search_memory",
"description": "Semantic recall from the Supermemory store. Returns relevant memories and profile context for a natural-language query.",
"inputSchema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Natural-language search query."},
"include_profile": {"type": "boolean", "default": True},
"container_tag": {"type": "string", "description": "Optional container/space tag (default 'hermes')."}
},
"required": ["query"]
}
},
{
"name": "add_memory",
"description": "Save information to Supermemory (action='save') or mark it for forgetting (action='forget').",
"inputSchema": {
"type": "object",
"properties": {
"content": {"type": "string", "description": "The memory text to save."},
"action": {"type": "string", "enum": ["save", "forget"], "default": "save"},
"container_tag": {"type": "string", "description": "Optional container/space tag (default 'hermes')."}
},
"required": ["content"]
}
},
{
"name": "list_documents",
"description": "Browse stored source documents (IDs, titles, types, status).",
"inputSchema": {
"type": "object",
"properties": {
"page": {"type": "integer", "default": 1},
"limit": {"type": "integer", "default": 10},
"container_tag": {"type": "string"}
}
}
},
{
"name": "get_document",
"description": "Read the available content of one document by ID.",
"inputSchema": {
"type": "object",
"properties": {
"document_id": {"type": "string", "description": "Document ID."},
"container_tag": {"type": "string"}
},
"required": ["document_id"]
}
},
{
"name": "list_memories",
"description": "Browse recent extracted memory entries.",
"inputSchema": {
"type": "object",
"properties": {
"page": {"type": "integer", "default": 1},
"limit": {"type": "integer", "default": 10},
"container_tag": {"type": "string"}
}
}
},
{
"name": "whoami",
"description": "Report the active container tag and server status.",
"inputSchema": {"type": "object", "properties": {}}
},
]
HANDLERS = {
"search_memory": lambda a: search_memory(a.get("query", ""), a.get("include_profile", True), a.get("container_tag")),
"add_memory": lambda a: add_memory(a.get("content", ""), a.get("action", "save"), a.get("container_tag")),
"list_documents": lambda a: list_documents(a.get("page", 1), a.get("limit", 10), a.get("container_tag")),
"get_document": lambda a: get_document(a.get("document_id", ""), a.get("container_tag")),
"list_memories": lambda a: list_memories(a.get("page", 1), a.get("limit", 10), a.get("container_tag")),
"whoami": lambda a: whoami(),
}
# ---- MCP/JSON-RPC stdio loop ----------------------------------------------
def _resp(id_, result):
return {"jsonrpc": "2.0", "id": id_, "result": result}
def _main():
while True:
line = sys.stdin.readline()
if not line:
break
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue
method = msg.get("method")
mid = msg.get("id")
params = msg.get("params", {}) or {}
if method == "initialize":
result = {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "supermemory-local", "version": "1.0.0"}
}
sys.stdout.write(json.dumps(_resp(mid, result)) + "\n")
sys.stdout.flush()
elif method == "notifications/initialized":
continue
elif method == "tools/list":
sys.stdout.write(json.dumps(_resp(mid, {"tools": TOOLS})) + "\n")
sys.stdout.flush()
elif method == "tools/call":
name = params.get("name")
args = params.get("arguments", {}) or {}
handler = HANDLERS.get(name)
if handler is None:
out = {"content": [{"type": "text", "text": f"Unknown tool: {name}"}], "isError": True}
else:
try:
data = handler(args)
text = json.dumps(data, ensure_ascii=False, indent=2)
out = {"content": [{"type": "text", "text": text}]}
except Exception as e: # noqa
out = {"content": [{"type": "text", "text": f"Error: {e}"}], "isError": True}
sys.stdout.write(json.dumps(_resp(mid, out)) + "\n")
sys.stdout.flush()
else:
if mid is not None:
sys.stdout.write(json.dumps(_resp(mid, {})) + "\n")
sys.stdout.flush()
if __name__ == "__main__":
_main()

View file

@ -0,0 +1,109 @@
#!/usr/bin/env bash
# supermemory-sync — keep local Supermemory in sync with the user's coding projects.
#
# Subcommands:
# sync-rules Re-ingest CLAUDE.md/AGENTS.md per project only if changed (hash-gated).
# remember-commit Feed a git commit summary into the project's container.
# args: <project> <commit-msg> [files-changed...]
# remember-bug Feed a bug/fix note into the project's container.
# args: <project> <note>
#
# Requires: local Supermemory server running on http://localhost:6767 and
# ~/.supermemory/api-key present. All projects map to a container_tag.
set -euo pipefail
SUPERMEMORY_KEY_FILE="$HOME/.supermemory/api-key"
BASE_URL="http://localhost:6767"
STATE_FILE="$HOME/.supermemory/sync-state.json"
# Project -> container_tag + path to its rule files
declare -A PROJECTS=(
[dafgpt]="/media/themorida/647ECE1E7ECDE93E/Elementary OS/DafGpt"
[hejle]="/media/themorida/647ECE1E7ECDE93E/Elementary OS/hejle"
[limbik]="/media/themorida/647ECE1E7ECDE93E/Elementary OS/Limbik"
)
api_key() {
cat "$SUPERMEMORY_KEY_FILE" 2>/dev/null || true
}
post_doc() {
# $1=containerTag $2=content $3=type
local ct="$1" content="$2" type="$3"
local k; k=$(api_key)
local payload
payload=$(python3 - "$ct" "$content" "$type" <<'PY'
import json, sys
ct, content, type_ = sys.argv[1], sys.argv[2], sys.argv[3]
print(json.dumps({"content": content, "containerTag": ct,
"metadata": {"type": type_}, "dreaming": "instant"}))
PY
)
curl -s -X POST "$BASE_URL/v3/documents" \
-H "Authorization: Bearer $k" -H "Content-Type: application/json" \
-d "$payload" >/dev/null 2>&1 || true
}
hash_of() {
local f="$1"
[ -f "$f" ] && sha256sum "$f" | cut -d' ' -f1 || echo "missing"
}
cmd_sync_rules() {
local state='{}'
[ -f "$STATE_FILE" ] && state=$(cat "$STATE_FILE")
local new_state='{}'
for p in "${!PROJECTS[@]}"; do
dir="${PROJECTS[$p]}"
rules=""
for rf in "$dir/CLAUDE.md" "$dir/AGENTS.md"; do
[ -f "$rf" ] && rules+="$(cat "$rf" 2>/dev/null)
"
done
h=$(printf '%s' "$rules" | sha256sum | cut -d' ' -f1)
old=$(echo "$state" | python3 -c "import json,sys; print(json.load(sys.stdin).get('$p',''))" 2>/dev/null || true)
if [ -n "$rules" ]; then
if [ "$h" != "$old" ]; then
echo " [$p] rules changed -> re-ingesting"
post_doc "$p" "$rules" "project-rules"
else
echo " [$p] unchanged"
fi
new_state=$(echo "$new_state" | python3 -c "import json,sys; d=json.load(sys.stdin); d['$p']='$h'; print(json.dumps(d))" 2>/dev/null || echo "$new_state")
fi
done
echo "$new_state" > "$STATE_FILE"
echo "sync-rules done"
}
cmd_remember_commit() {
local project="$1"; shift || true
local msg="${1:-}"; shift || true
[ -z "$project" ] && { echo "usage: remember-commit <project> <msg> [files...]" >&2; exit 1; }
local files="$*"
local ct="${PROJECTS[$project]:-}"
[ -z "$ct" ] && { echo "unknown project: $project" >&2; exit 1; }
local note="[commit] $msg
Changed files: ${files:-n/a}
Project: $project"
post_doc "$project" "$note" "commit"
echo "remembered commit for $project"
}
cmd_remember_bug() {
local project="$1"; shift || true
local note="$*"
[ -z "$project" ] && { echo "usage: remember-bug <project> <note>" >&2; exit 1; }
local ct="${PROJECTS[$project]:-}"
[ -z "$ct" ] && { echo "unknown project: $project" >&2; exit 1; }
post_doc "$project" "[bug-fix] $note" "bug-fix"
echo "remembered bug/fix for $project"
}
case "${1:-}" in
sync-rules) cmd_sync_rules ;;
remember-commit) shift; cmd_remember_commit "$@" ;;
remember-bug) shift; cmd_remember_bug "$@" ;;
*) echo "usage: supermemory-sync {sync-rules|remember-commit <p> <msg> [files]|remember-bug <p> <note>}" >&2; exit 1 ;;
esac

View file

@ -0,0 +1,6 @@
[Unit]
Description=Supermemory project-rules sync (daily)
[Service]
Type=oneshot
ExecStart=/home/themorida/.local/bin/supermemory-sync sync-rules

View file

@ -0,0 +1,9 @@
[Unit]
Description=Supermemory project-rules sync timer
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,17 @@
[Unit]
Description=Supermemory local memory server (self-hosted)
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/.supermemory
Environment=SUPERMEMORY_DATA_DIR=%h/.supermemory
# Replace the value below with a real Gemini/OpenAI/Anthropic/Groq API key.
# The local server needs an LLM key to perform memory extraction.
Environment=GEMINI_API_KEY=__REPLACE_WITH_YOUR_LLM_KEY__
ExecStart=%h/.supermemory/bin/supermemory-server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target